commit d6f4b4f3eae3778179a9d05a8dfbf38b016af256 Author: server Date: Wed Nov 17 11:47:22 2021 -0700 python-chess on cartesi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a09370a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.ext2 diff --git a/README.md b/README.md new file mode 100644 index 0000000..72e0ebe --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Cartesi python-chess +Crufty scripts to build disk images to run under the Cartesi +RISC-V machine emulator that verify a chess game. Similar +to the Cartesi example, but this is in python using python-chess, +not javascript. + + +# Upstream + +## Python Chess + +* https://python-chess.readthedocs.io/en/latest/ + +* https://github.com/niklasf/python-chess/ + +# # Cartesi + +* https://cartesi.io/docs/intro + +* https://github.com/cartesi/poker/tree/master/blockchain/verifier/chess + +* https://medium.com/cartesi/how-cartesi-is-changing-the-game-showcasing-chess-9f4e39d5cea0 diff --git a/build-cartesi-cartesi-python-chess b/build-cartesi-cartesi-python-chess new file mode 100755 index 0000000..9cbb4d1 --- /dev/null +++ b/build-cartesi-cartesi-python-chess @@ -0,0 +1,5 @@ +#!/bin/bash + +# Make disk image with script +genext2fs -b 1024 -d cartesi-python-chess-cartesi-img cartesi-python-chess.ext2 + diff --git a/cartesi-buildroot-config b/cartesi-buildroot-config new file mode 100644 index 0000000..65411d3 --- /dev/null +++ b/cartesi-buildroot-config @@ -0,0 +1,3481 @@ +# +# Automatically generated file; DO NOT EDIT. +# Buildroot 2020.08.1-dirty Configuration +# +BR2_HAVE_DOT_CONFIG=y +BR2_HOST_GCC_AT_LEAST_4_9=y +BR2_HOST_GCC_AT_LEAST_5=y +BR2_HOST_GCC_AT_LEAST_6=y +BR2_HOST_GCC_AT_LEAST_7=y +BR2_HOST_GCC_AT_LEAST_8=y +BR2_HOST_GCC_AT_LEAST_9=y + +# +# Target options +# +BR2_ARCH_IS_64=y +BR2_ARCH_HAS_MMU_MANDATORY=y +# BR2_arcle is not set +# BR2_arceb is not set +# BR2_arm is not set +# BR2_armeb is not set +# BR2_aarch64 is not set +# BR2_aarch64_be is not set +# BR2_csky is not set +# BR2_i386 is not set +# BR2_m68k is not set +# BR2_microblazeel is not set +# BR2_microblazebe is not set +# BR2_mips is not set +# BR2_mipsel is not set +# BR2_mips64 is not set +# BR2_mips64el is not set +# BR2_nds32 is not set +# BR2_nios2 is not set +# BR2_or1k is not set +# BR2_powerpc is not set +# BR2_powerpc64 is not set +# BR2_powerpc64le is not set +BR2_riscv=y +# BR2_sh is not set +# BR2_sparc is not set +# BR2_sparc64 is not set +# BR2_x86_64 is not set +# BR2_xtensa is not set +BR2_ARCH_HAS_TOOLCHAIN_BUILDROOT=y +BR2_ARCH_NEEDS_GCC_AT_LEAST_4_8=y +BR2_ARCH_NEEDS_GCC_AT_LEAST_4_9=y +BR2_ARCH_NEEDS_GCC_AT_LEAST_5=y +BR2_ARCH_NEEDS_GCC_AT_LEAST_6=y +BR2_ARCH_NEEDS_GCC_AT_LEAST_7=y +BR2_ARCH="riscv64" +BR2_ENDIAN="LITTLE" +BR2_GCC_TARGET_ABI="lp64" +BR2_BINFMT_SUPPORTS_SHARED=y +BR2_READELF_ARCH_NAME="RISC-V" +BR2_BINFMT_ELF=y +BR2_RISCV_ISA_RVI=y +BR2_RISCV_ISA_RVM=y +BR2_RISCV_ISA_RVA=y +# BR2_riscv_g is not set +BR2_riscv_custom=y + +# +# Instruction Set Extensions +# +BR2_RISCV_ISA_CUSTOM_RVM=y +BR2_RISCV_ISA_CUSTOM_RVA=y +# BR2_RISCV_ISA_CUSTOM_RVF is not set +# BR2_RISCV_ISA_CUSTOM_RVC is not set +# BR2_RISCV_32 is not set +BR2_RISCV_64=y +BR2_RISCV_ABI_LP64=y + +# +# Build options +# + +# +# Commands +# +BR2_WGET="wget --passive-ftp -nd -t 3" +BR2_SVN="svn" +BR2_BZR="bzr" +BR2_GIT="git" +BR2_CVS="cvs" +BR2_LOCALFILES="cp" +BR2_SCP="scp" +BR2_HG="hg" +BR2_ZCAT="gzip -d -c" +BR2_BZCAT="bzcat" +BR2_XZCAT="xzcat" +BR2_LZCAT="lzip -d -c" +BR2_TAR_OPTIONS="" +BR2_DEFCONFIG="$(CONFIG_DIR)/defconfig" +BR2_DL_DIR="$(TOPDIR)/dl" +BR2_HOST_DIR="$(BASE_DIR)/host" + +# +# Mirrors and Download locations +# +BR2_PRIMARY_SITE="" +BR2_BACKUP_SITE="http://sources.buildroot.net" +BR2_KERNEL_MIRROR="https://cdn.kernel.org/pub" +BR2_GNU_MIRROR="http://ftpmirror.gnu.org" +BR2_LUAROCKS_MIRROR="http://rocks.moonscript.org" +BR2_CPAN_MIRROR="http://cpan.metacpan.org" +BR2_JLEVEL=0 +BR2_CCACHE=y +BR2_CCACHE_DIR="$(HOME)/.buildroot-ccache" +BR2_CCACHE_INITIAL_SETUP="" +BR2_CCACHE_USE_BASEDIR=y +# BR2_ENABLE_DEBUG is not set +BR2_STRIP_strip=y +BR2_STRIP_EXCLUDE_FILES="" +BR2_STRIP_EXCLUDE_DIRS="" +# BR2_OPTIMIZE_0 is not set +# BR2_OPTIMIZE_1 is not set +# BR2_OPTIMIZE_2 is not set +# BR2_OPTIMIZE_3 is not set +# BR2_OPTIMIZE_G is not set +BR2_OPTIMIZE_S=y +# BR2_OPTIMIZE_FAST is not set +# BR2_STATIC_LIBS is not set +BR2_SHARED_LIBS=y +# BR2_SHARED_STATIC_LIBS is not set +BR2_PACKAGE_OVERRIDE_FILE="$(CONFIG_DIR)/local.mk" +BR2_GLOBAL_PATCH_DIR="" + +# +# Advanced +# +BR2_COMPILER_PARANOID_UNSAFE_PATH=y +# BR2_FORCE_HOST_BUILD is not set +# BR2_REPRODUCIBLE is not set +# BR2_PER_PACKAGE_DIRECTORIES is not set + +# +# Security Hardening Options +# +# BR2_PIC_PIE is not set +BR2_SSP_NONE=y +# BR2_SSP_REGULAR is not set +# BR2_SSP_STRONG is not set +# BR2_SSP_ALL is not set +BR2_RELRO_NONE=y +# BR2_RELRO_PARTIAL is not set +# BR2_RELRO_FULL is not set +BR2_FORTIFY_SOURCE_NONE=y +# BR2_FORTIFY_SOURCE_1 is not set +# BR2_FORTIFY_SOURCE_2 is not set + +# +# Toolchain +# +BR2_TOOLCHAIN=y +BR2_TOOLCHAIN_USES_GLIBC=y +# BR2_TOOLCHAIN_BUILDROOT is not set +BR2_TOOLCHAIN_EXTERNAL=y + +# +# Toolchain External Options +# +BR2_TOOLCHAIN_EXTERNAL_CUSTOM=y +# BR2_TOOLCHAIN_EXTERNAL_DOWNLOAD is not set +BR2_TOOLCHAIN_EXTERNAL_PREINSTALLED=y +BR2_TOOLCHAIN_EXTERNAL_PATH="$(RISCV)" +BR2_TOOLCHAIN_EXTERNAL_GLIBC=y +BR2_PACKAGE_HAS_TOOLCHAIN_EXTERNAL=y +BR2_PACKAGE_PROVIDES_TOOLCHAIN_EXTERNAL="toolchain-external-custom" +BR2_TOOLCHAIN_EXTERNAL_PREFIX="riscv64-cartesi-linux-gnu" +BR2_TOOLCHAIN_EXTERNAL_CUSTOM_PREFIX="riscv64-cartesi-linux-gnu" +BR2_TOOLCHAIN_EXTERNAL_GCC_10=y +# BR2_TOOLCHAIN_EXTERNAL_GCC_9 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_8 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_7 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_6 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_5 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_4_9 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_4_8 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_4_7 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_4_6 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_4_5 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_4_4 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_4_3 is not set +# BR2_TOOLCHAIN_EXTERNAL_GCC_OLD is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_5_7 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_5_6 is not set +BR2_TOOLCHAIN_EXTERNAL_HEADERS_5_5=y +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_5_4 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_5_3 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_5_2 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_5_1 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_5_0 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_20 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_19 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_18 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_17 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_16 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_15 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_14 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_13 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_12 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_11 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_10 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_9 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_8 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_7 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_6 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_5 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_4 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_3 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_2 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_1 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_0 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_19 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_18 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_17 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_16 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_15 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_14 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_13 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_12 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_11 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_10 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_9 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_8 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_7 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_6 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_5 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_4 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_3 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_2 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_1 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_3_0 is not set +# BR2_TOOLCHAIN_EXTERNAL_HEADERS_REALLY_OLD is not set +BR2_TOOLCHAIN_EXTERNAL_HAS_CARTESI_KERNEL_HEADERS=y +# BR2_TOOLCHAIN_EXTERNAL_CUSTOM_UCLIBC is not set +BR2_TOOLCHAIN_EXTERNAL_CUSTOM_GLIBC=y +# BR2_TOOLCHAIN_EXTERNAL_CUSTOM_MUSL is not set +BR2_TOOLCHAIN_EXTERNAL_HAS_SSP=y +BR2_TOOLCHAIN_EXTERNAL_HAS_SSP_STRONG=y +# BR2_TOOLCHAIN_EXTERNAL_INET_RPC is not set +BR2_TOOLCHAIN_EXTERNAL_CXX=y +# BR2_TOOLCHAIN_EXTERNAL_DLANG is not set +# BR2_TOOLCHAIN_EXTERNAL_FORTRAN is not set +# BR2_TOOLCHAIN_EXTERNAL_OPENMP is not set +# BR2_TOOLCHAIN_EXTERNAL_GDB_SERVER_COPY is not set + +# +# Toolchain Generic Options +# +BR2_TOOLCHAIN_SUPPORTS_ALWAYS_LOCKFREE_ATOMIC_INTS=y +BR2_TOOLCHAIN_SUPPORTS_VARIADIC_MI_THUNK=y +BR2_USE_WCHAR=y +BR2_ENABLE_LOCALE=y +BR2_INSTALL_LIBSTDCPP=y +BR2_TOOLCHAIN_HAS_THREADS=y +BR2_TOOLCHAIN_HAS_THREADS_DEBUG=y +BR2_TOOLCHAIN_HAS_THREADS_NPTL=y +BR2_TOOLCHAIN_HAS_SSP=y +BR2_TOOLCHAIN_HAS_SSP_STRONG=y +BR2_TOOLCHAIN_HAS_UCONTEXT=y +BR2_TOOLCHAIN_SUPPORTS_PIE=y +# BR2_TOOLCHAIN_GLIBC_GCONV_LIBS_COPY is not set +BR2_TOOLCHAIN_EXTRA_LIBS="" +BR2_TOOLCHAIN_HAS_FULL_GETTEXT=y +BR2_USE_MMU=y +BR2_TARGET_OPTIMIZATION="" +BR2_TARGET_LDFLAGS="" +# BR2_ECLIPSE_REGISTER is not set +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_0=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_1=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_2=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_3=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_4=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_5=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_6=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_7=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_8=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_9=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_10=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_11=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_12=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_13=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_14=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_15=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_16=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_17=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_18=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_3_19=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_0=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_1=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_2=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_3=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_4=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_5=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_6=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_7=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_8=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_9=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_10=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_11=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_12=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_13=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_14=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_15=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_16=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_17=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_18=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_19=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_4_20=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_5_0=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_5_1=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_5_2=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_5_3=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_5_4=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST_5_5=y +BR2_TOOLCHAIN_HEADERS_AT_LEAST="5.5" +BR2_TOOLCHAIN_GCC_AT_LEAST_4_3=y +BR2_TOOLCHAIN_GCC_AT_LEAST_4_4=y +BR2_TOOLCHAIN_GCC_AT_LEAST_4_5=y +BR2_TOOLCHAIN_GCC_AT_LEAST_4_6=y +BR2_TOOLCHAIN_GCC_AT_LEAST_4_7=y +BR2_TOOLCHAIN_GCC_AT_LEAST_4_8=y +BR2_TOOLCHAIN_GCC_AT_LEAST_4_9=y +BR2_TOOLCHAIN_GCC_AT_LEAST_5=y +BR2_TOOLCHAIN_GCC_AT_LEAST_6=y +BR2_TOOLCHAIN_GCC_AT_LEAST_7=y +BR2_TOOLCHAIN_GCC_AT_LEAST_8=y +BR2_TOOLCHAIN_GCC_AT_LEAST_9=y +BR2_TOOLCHAIN_GCC_AT_LEAST_10=y +BR2_TOOLCHAIN_GCC_AT_LEAST="10" +BR2_TOOLCHAIN_HAS_MNAN_OPTION=y +BR2_TOOLCHAIN_HAS_SYNC_1=y +BR2_TOOLCHAIN_HAS_SYNC_2=y +BR2_TOOLCHAIN_HAS_SYNC_4=y +BR2_TOOLCHAIN_HAS_SYNC_8=y +BR2_TOOLCHAIN_HAS_LIBATOMIC=y +BR2_TOOLCHAIN_HAS_ATOMIC=y + +# +# System configuration +# +BR2_ROOTFS_SKELETON_DEFAULT=y +# BR2_ROOTFS_SKELETON_CUSTOM is not set +BR2_TARGET_GENERIC_HOSTNAME="cartesi-machine" +BR2_TARGET_GENERIC_ISSUE="Welcome to Cartesi" +BR2_TARGET_GENERIC_PASSWD_SHA256=y +# BR2_TARGET_GENERIC_PASSWD_SHA512 is not set +BR2_TARGET_GENERIC_PASSWD_METHOD="sha-256" +# BR2_INIT_BUSYBOX is not set +# BR2_INIT_SYSV is not set +# BR2_INIT_OPENRC is not set +# BR2_INIT_SYSTEMD is not set +BR2_INIT_NONE=y +# BR2_ROOTFS_DEVICE_CREATION_STATIC is not set +BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_DEVTMPFS=y +# BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_MDEV is not set +# BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV is not set +BR2_ROOTFS_DEVICE_TABLE="" +# BR2_ROOTFS_DEVICE_TABLE_SUPPORTS_EXTENDED_ATTRIBUTES is not set +# BR2_ROOTFS_MERGED_USR is not set +# BR2_TARGET_ENABLE_ROOT_LOGIN is not set +BR2_SYSTEM_BIN_SH_BUSYBOX=y + +# +# bash, dash, mksh, zsh need BR2_PACKAGE_BUSYBOX_SHOW_OTHERS +# +# BR2_SYSTEM_BIN_SH_NONE is not set +# BR2_TARGET_GENERIC_GETTY is not set +# BR2_TARGET_GENERIC_REMOUNT_ROOTFS_RW is not set +BR2_SYSTEM_DHCP="" +BR2_SYSTEM_DEFAULT_PATH="/bin:/sbin:/usr/bin:/usr/sbin" +# BR2_ENABLE_LOCALE_PURGE is not set +BR2_GENERATE_LOCALE="" +# BR2_SYSTEM_ENABLE_NLS is not set +# BR2_TARGET_TZ_INFO is not set +BR2_ROOTFS_USERS_TABLES="" +BR2_ROOTFS_OVERLAY="skel" +BR2_ROOTFS_POST_BUILD_SCRIPT="" +BR2_ROOTFS_POST_FAKEROOT_SCRIPT="" +BR2_ROOTFS_POST_IMAGE_SCRIPT="" + +# +# Kernel +# +# BR2_LINUX_KERNEL is not set + +# +# Target packages +# +BR2_PACKAGE_BUSYBOX=y +BR2_PACKAGE_BUSYBOX_CONFIG="package/busybox/busybox.config" +BR2_PACKAGE_BUSYBOX_CONFIG_FRAGMENT_FILES="cartesi-busybox-fragment" +# BR2_PACKAGE_BUSYBOX_SHOW_OTHERS is not set +# BR2_PACKAGE_BUSYBOX_INDIVIDUAL_BINARIES is not set +# BR2_PACKAGE_BUSYBOX_WATCHDOG is not set +BR2_PACKAGE_SKELETON=y +BR2_PACKAGE_HAS_SKELETON=y +BR2_PACKAGE_PROVIDES_SKELETON="skeleton-init-none" +BR2_PACKAGE_SKELETON_INIT_COMMON=y +BR2_PACKAGE_SKELETON_INIT_NONE=y + +# +# Audio and video applications +# +# BR2_PACKAGE_ALSA_UTILS is not set +# BR2_PACKAGE_ATEST is not set +# BR2_PACKAGE_AUMIX is not set +# BR2_PACKAGE_BELLAGIO is not set +# BR2_PACKAGE_BLUEZ_ALSA is not set +# BR2_PACKAGE_DVBLAST is not set +# BR2_PACKAGE_DVDAUTHOR is not set +# BR2_PACKAGE_DVDRW_TOOLS is not set +# BR2_PACKAGE_ESPEAK is not set +# BR2_PACKAGE_FAAD2 is not set +BR2_PACKAGE_FFMPEG_ARCH_SUPPORTS=y +# BR2_PACKAGE_FFMPEG is not set +# BR2_PACKAGE_FLAC is not set +# BR2_PACKAGE_FLITE is not set +# BR2_PACKAGE_FLUID_SOUNDFONT is not set +# BR2_PACKAGE_FLUIDSYNTH is not set +# BR2_PACKAGE_GMRENDER_RESURRECT is not set +# BR2_PACKAGE_GSTREAMER1 is not set +# BR2_PACKAGE_JACK1 is not set +# BR2_PACKAGE_JACK2 is not set +BR2_PACKAGE_KODI_ARCH_SUPPORTS=y + +# +# kodi needs python w/ .py modules, a uClibc or glibc toolchain w/ C++, threads, wchar, dynamic library, gcc >= 4.8 +# + +# +# kodi needs an OpenGL EGL backend with OpenGL support +# +# BR2_PACKAGE_LAME is not set +# BR2_PACKAGE_MADPLAY is not set +# BR2_PACKAGE_MIMIC is not set +# BR2_PACKAGE_MINIMODEM is not set + +# +# miraclecast needs systemd and a glibc toolchain w/ threads and wchar +# +# BR2_PACKAGE_MJPEGTOOLS is not set +# BR2_PACKAGE_MODPLUGTOOLS is not set +# BR2_PACKAGE_MOTION is not set +# BR2_PACKAGE_MPD is not set +# BR2_PACKAGE_MPD_MPC is not set +# BR2_PACKAGE_MPG123 is not set +# BR2_PACKAGE_MPV is not set +# BR2_PACKAGE_MULTICAT is not set +# BR2_PACKAGE_MUSEPACK is not set +# BR2_PACKAGE_NCMPC is not set +# BR2_PACKAGE_OPUS_TOOLS is not set +BR2_PACKAGE_PULSEAUDIO_HAS_ATOMIC=y +# BR2_PACKAGE_PULSEAUDIO is not set +# BR2_PACKAGE_SOX is not set +# BR2_PACKAGE_SQUEEZELITE is not set +# BR2_PACKAGE_TOVID is not set +# BR2_PACKAGE_TSTOOLS is not set +# BR2_PACKAGE_TWOLAME is not set +# BR2_PACKAGE_UDPXY is not set +# BR2_PACKAGE_UPMPDCLI is not set +# BR2_PACKAGE_V4L2GRAB is not set + +# +# v4l2loopback needs a Linux kernel to be built +# +# BR2_PACKAGE_VLC is not set +# BR2_PACKAGE_VORBIS_TOOLS is not set +# BR2_PACKAGE_WAVPACK is not set +# BR2_PACKAGE_YAVTA is not set +# BR2_PACKAGE_YMPD is not set + +# +# Compressors and decompressors +# +BR2_PACKAGE_BROTLI=y +BR2_PACKAGE_BZIP2=y +# BR2_PACKAGE_LRZIP is not set +BR2_PACKAGE_LZIP=y +BR2_PACKAGE_LZOP=y +BR2_PACKAGE_P7ZIP=y +# BR2_PACKAGE_PIGZ is not set +# BR2_PACKAGE_PIXZ is not set +BR2_PACKAGE_UNRAR=y +BR2_PACKAGE_XZ=y +BR2_PACKAGE_ZIP=y +# BR2_PACKAGE_ZSTD is not set + +# +# Debugging, profiling and benchmark +# +# BR2_PACKAGE_BLKTRACE is not set +# BR2_PACKAGE_BONNIE is not set +# BR2_PACKAGE_CACHE_CALIBRATOR is not set + +# +# clinfo needs an OpenCL provider +# + +# +# dacapo needs OpenJDK +# +BR2_PACKAGE_DHRYSTONE=y +# BR2_PACKAGE_DIEHARDER is not set +# BR2_PACKAGE_DMALLOC is not set +# BR2_PACKAGE_DROPWATCH is not set +# BR2_PACKAGE_DSTAT is not set +# BR2_PACKAGE_DT is not set +# BR2_PACKAGE_DUMA is not set +# BR2_PACKAGE_FIO is not set +BR2_PACKAGE_GDB_ARCH_SUPPORTS=y +# BR2_PACKAGE_GDB is not set +# BR2_PACKAGE_IOZONE is not set + +# +# ktap needs a Linux kernel to be built +# +# BR2_PACKAGE_KYUA is not set +# BR2_PACKAGE_LATENCYTOP is not set +# BR2_PACKAGE_LMBENCH is not set +BR2_PACKAGE_LTP_TESTSUITE_ARCH_SUPPORTS=y +# BR2_PACKAGE_LTP_TESTSUITE is not set +# BR2_PACKAGE_LTTNG_BABELTRACE is not set + +# +# lttng-modules needs a Linux kernel to be built +# +# BR2_PACKAGE_LTTNG_TOOLS is not set +# BR2_PACKAGE_MEMSTAT is not set +# BR2_PACKAGE_NETPERF is not set +# BR2_PACKAGE_NETSNIFF_NG is not set +# BR2_PACKAGE_NMON is not set +# BR2_PACKAGE_PAX_UTILS is not set +# BR2_PACKAGE_PV is not set +# BR2_PACKAGE_RAMSMP is not set +# BR2_PACKAGE_RAMSPEED is not set +# BR2_PACKAGE_SPIDEV_TEST is not set +BR2_PACKAGE_STRACE=y +# BR2_PACKAGE_STRESS is not set +# BR2_PACKAGE_STRESS_NG is not set + +# +# sysdig needs a glibc or uclibc toolchain w/ C++, threads, gcc >= 4.8, dynamic library, a Linux kernel, and luajit or lua 5.1 to be built +# +# BR2_PACKAGE_TINYMEMBENCH is not set +# BR2_PACKAGE_TRACE_CMD is not set +# BR2_PACKAGE_UCLIBC_NG_TEST is not set +# BR2_PACKAGE_VMTOUCH is not set +BR2_PACKAGE_WHETSTONE=y + +# +# Development tools +# +# BR2_PACKAGE_BINUTILS is not set +# BR2_PACKAGE_BITWISE is not set +# BR2_PACKAGE_BSDIFF is not set +# BR2_PACKAGE_CHECK is not set +# BR2_PACKAGE_CPPUNIT is not set +# BR2_PACKAGE_CUNIT is not set +# BR2_PACKAGE_CVS is not set +# BR2_PACKAGE_CXXTEST is not set +# BR2_PACKAGE_FLEX is not set +# BR2_PACKAGE_GETTEXT is not set +BR2_PACKAGE_PROVIDES_HOST_GETTEXT="host-gettext-tiny" +# BR2_PACKAGE_GIT is not set +# BR2_PACKAGE_GIT_CRYPT is not set +# BR2_PACKAGE_GPERF is not set +# BR2_PACKAGE_JO is not set +BR2_PACKAGE_JQ=y +# BR2_PACKAGE_LIBTOOL is not set +BR2_PACKAGE_MAKE=y +# BR2_PACKAGE_PKGCONF is not set +# BR2_PACKAGE_SUBVERSION is not set +# BR2_PACKAGE_TREE is not set + +# +# Filesystem and flash utilities +# +# BR2_PACKAGE_ABOOTIMG is not set + +# +# aufs-util needs a linux kernel and a toolchain w/ threads +# +# BR2_PACKAGE_AUTOFS is not set +# BR2_PACKAGE_BTRFS_PROGS is not set +# BR2_PACKAGE_CIFS_UTILS is not set +BR2_PACKAGE_CPIO=y +# BR2_PACKAGE_CRAMFS is not set +# BR2_PACKAGE_CURLFTPFS is not set +# BR2_PACKAGE_DAVFS2 is not set +BR2_PACKAGE_DEVIO=y +# BR2_PACKAGE_DOSFSTOOLS is not set +BR2_PACKAGE_E2FSPROGS=y +# BR2_PACKAGE_E2FSPROGS_DEBUGFS is not set +# BR2_PACKAGE_E2FSPROGS_E2IMAGE is not set +# BR2_PACKAGE_E2FSPROGS_E4DEFRAG is not set +# BR2_PACKAGE_E2FSPROGS_FUSE2FS is not set +# BR2_PACKAGE_E2FSPROGS_RESIZE2FS is not set +BR2_PACKAGE_E2TOOLS=y +# BR2_PACKAGE_ECRYPTFS_UTILS is not set +# BR2_PACKAGE_EROFS_UTILS is not set +# BR2_PACKAGE_EXFAT is not set +# BR2_PACKAGE_EXFAT_UTILS is not set +# BR2_PACKAGE_EXFATPROGS is not set +# BR2_PACKAGE_F2FS_TOOLS is not set +# BR2_PACKAGE_FLASHBENCH is not set +# BR2_PACKAGE_FSCRYPTCTL is not set +# BR2_PACKAGE_FUSE_OVERLAYFS is not set +# BR2_PACKAGE_FWUP is not set +BR2_PACKAGE_GENEXT2FS=y +# BR2_PACKAGE_GENPART is not set +# BR2_PACKAGE_GENROMFS is not set +# BR2_PACKAGE_IMX_USB_LOADER is not set +# BR2_PACKAGE_MMC_UTILS is not set +# BR2_PACKAGE_MTD is not set +# BR2_PACKAGE_MTOOLS is not set +# BR2_PACKAGE_NFS_UTILS is not set +# BR2_PACKAGE_NILFS_UTILS is not set +# BR2_PACKAGE_NTFS_3G is not set +# BR2_PACKAGE_SP_OOPS_EXTRACT is not set +# BR2_PACKAGE_SQUASHFS is not set +# BR2_PACKAGE_SSHFS is not set +# BR2_PACKAGE_UDFTOOLS is not set +# BR2_PACKAGE_UNIONFS is not set +# BR2_PACKAGE_XFSPROGS is not set + +# +# Fonts, cursors, icons, sounds and themes +# + +# +# Cursors +# +# BR2_PACKAGE_COMIX_CURSORS is not set +# BR2_PACKAGE_OBSIDIAN_CURSORS is not set + +# +# Fonts +# +# BR2_PACKAGE_BITSTREAM_VERA is not set +# BR2_PACKAGE_CANTARELL is not set +# BR2_PACKAGE_DEJAVU is not set +# BR2_PACKAGE_FONT_AWESOME is not set +# BR2_PACKAGE_GHOSTSCRIPT_FONTS is not set +# BR2_PACKAGE_INCONSOLATA is not set +# BR2_PACKAGE_LIBERATION is not set + +# +# Icons +# +# BR2_PACKAGE_GOOGLE_MATERIAL_DESIGN_ICONS is not set +# BR2_PACKAGE_HICOLOR_ICON_THEME is not set + +# +# Sounds +# +# BR2_PACKAGE_SOUND_THEME_BOREALIS is not set +# BR2_PACKAGE_SOUND_THEME_FREEDESKTOP is not set + +# +# Themes +# + +# +# Games +# +# BR2_PACKAGE_ASCII_INVADERS is not set +# BR2_PACKAGE_CHOCOLATE_DOOM is not set +# BR2_PACKAGE_FLARE_ENGINE is not set +# BR2_PACKAGE_GNUCHESS is not set +# BR2_PACKAGE_LBREAKOUT2 is not set +# BR2_PACKAGE_LTRIS is not set + +# +# minetest needs X11 and an OpenGL provider +# +# BR2_PACKAGE_OPENTYRIAN is not set +# BR2_PACKAGE_PRBOOM is not set +BR2_PACKAGE_SL=y + +# +# solarus needs OpenGL and a toolchain w/ C++, gcc >= 4.9, NPTL, dynamic library, and luajit or lua 5.1 +# +# BR2_PACKAGE_STELLA is not set + +# +# Graphic libraries and applications (graphic/text) +# + +# +# Graphic applications +# + +# +# cage needs udev, mesa3d w/ EGL and GLES support +# + +# +# cog needs wpewebkit and a toolchain w/ threads +# +# BR2_PACKAGE_FSWEBCAM is not set +# BR2_PACKAGE_GHOSTSCRIPT is not set + +# +# glmark2 needs an OpenGL or an openGL ES and EGL backend +# + +# +# glslsandbox-player needs a toolchain w/ threads and an openGL ES and EGL driver +# +# BR2_PACKAGE_GNUPLOT is not set +# BR2_PACKAGE_JHEAD is not set +# BR2_PACKAGE_LIBVA_UTILS is not set +BR2_PACKAGE_NETSURF_ARCH_SUPPORTS=y +# BR2_PACKAGE_NETSURF is not set +# BR2_PACKAGE_PNGQUANT is not set +# BR2_PACKAGE_RRDTOOL is not set + +# +# stellarium needs Qt5 and an OpenGL provider +# +# BR2_PACKAGE_TESSERACT_OCR is not set + +# +# Graphic libraries +# +# BR2_PACKAGE_CEGUI is not set +# BR2_PACKAGE_DIRECTFB is not set +# BR2_PACKAGE_FB_TEST_APP is not set +# BR2_PACKAGE_FBDUMP is not set +# BR2_PACKAGE_FBGRAB is not set +# BR2_PACKAGE_FBTERM is not set +# BR2_PACKAGE_FBV is not set +# BR2_PACKAGE_FREERDP is not set +# BR2_PACKAGE_IMAGEMAGICK is not set + +# +# linux-fusion needs a Linux kernel to be built +# +# BR2_PACKAGE_MESA3D is not set +# BR2_PACKAGE_OCRAD is not set + +# +# ogre needs X11 and an OpenGL provider +# +# BR2_PACKAGE_PSPLASH is not set +# BR2_PACKAGE_SDL is not set +# BR2_PACKAGE_SDL2 is not set + +# +# Other GUIs +# +# BR2_PACKAGE_QT5 is not set +# BR2_PACKAGE_TEKUI is not set + +# +# weston needs udev and a toolchain w/ locale, threads, dynamic library, headers >= 3.0 +# +# BR2_PACKAGE_XORG7 is not set +# BR2_PACKAGE_APITRACE is not set + +# +# vte needs an OpenGL or an OpenGL-EGL/wayland backend +# +# BR2_PACKAGE_XKEYBOARD_CONFIG is not set + +# +# Hardware handling +# + +# +# Firmware +# +# BR2_PACKAGE_ARMBIAN_FIRMWARE is not set +# BR2_PACKAGE_B43_FIRMWARE is not set +# BR2_PACKAGE_LINUX_FIRMWARE is not set +# BR2_PACKAGE_MURATA_CYW_FW is not set +# BR2_PACKAGE_UX500_FIRMWARE is not set +# BR2_PACKAGE_WILC1000_FIRMWARE is not set +# BR2_PACKAGE_WILINK_BT_FIRMWARE is not set +# BR2_PACKAGE_ZD1211_FIRMWARE is not set +# BR2_PACKAGE_18XX_TI_UTILS is not set +# BR2_PACKAGE_ACPICA is not set +# BR2_PACKAGE_ACPID is not set +# BR2_PACKAGE_ACPITOOL is not set +# BR2_PACKAGE_AER_INJECT is not set +# BR2_PACKAGE_APCUPSD is not set +# BR2_PACKAGE_AVRDUDE is not set + +# +# bcache-tools needs udev /dev management +# + +# +# brickd needs udev /dev management, a toolchain w/ threads, wchar +# +# BR2_PACKAGE_BRLTTY is not set +# BR2_PACKAGE_CC_TOOL is not set +# BR2_PACKAGE_CDRKIT is not set +# BR2_PACKAGE_CRYPTSETUP is not set +# BR2_PACKAGE_CWIID is not set + +# +# dahdi-linux needs a Linux kernel to be built +# + +# +# dahdi-tools needs a toolchain w/ threads and a Linux kernel to be built +# +# BR2_PACKAGE_DBUS is not set +# BR2_PACKAGE_DFU_UTIL is not set +# BR2_PACKAGE_DMRAID is not set + +# +# dt-utils needs udev /dev management +# +# BR2_PACKAGE_DTV_SCAN_TABLES is not set +# BR2_PACKAGE_DUMP1090 is not set +# BR2_PACKAGE_DVB_APPS is not set +# BR2_PACKAGE_DVBSNOOP is not set +# BR2_PACKAGE_EDID_DECODE is not set + +# +# edid-decode needs a toolchain w/ C++, gcc >= 4.7 +# + +# +# eudev needs eudev /dev management +# +# BR2_PACKAGE_EVEMU is not set +# BR2_PACKAGE_EVTEST is not set +# BR2_PACKAGE_FAN_CTRL is not set +# BR2_PACKAGE_FCONFIG is not set +BR2_PACKAGE_FLASHROM_ARCH_SUPPORTS=y +# BR2_PACKAGE_FLASHROM is not set +# BR2_PACKAGE_FMTOOLS is not set +# BR2_PACKAGE_FXLOAD is not set +# BR2_PACKAGE_GPM is not set +# BR2_PACKAGE_GPSD is not set +# BR2_PACKAGE_GPTFDISK is not set +# BR2_PACKAGE_GVFS is not set + +# +# gvfs needs a toolchain w/ wchar, threads, dynamic library +# +# BR2_PACKAGE_HWDATA is not set +# BR2_PACKAGE_HWLOC is not set +# BR2_PACKAGE_INPUT_EVENT_DAEMON is not set +# BR2_PACKAGE_IOSTAT is not set +# BR2_PACKAGE_IPMITOOL is not set +# BR2_PACKAGE_IRDA_UTILS is not set +# BR2_PACKAGE_KBD is not set +# BR2_PACKAGE_LCDPROC is not set +# BR2_PACKAGE_LIBUBOOTENV is not set +# BR2_PACKAGE_LIBUIO is not set + +# +# linux-backports needs a Linux kernel to be built +# +# BR2_PACKAGE_LINUX_SERIAL_TEST is not set +# BR2_PACKAGE_LINUXCONSOLETOOLS is not set +# BR2_PACKAGE_LIRC_TOOLS is not set +# BR2_PACKAGE_LM_SENSORS is not set +# BR2_PACKAGE_LSHW is not set +# BR2_PACKAGE_LSSCSI is not set +# BR2_PACKAGE_LSUIO is not set +# BR2_PACKAGE_LUKSMETA is not set +# BR2_PACKAGE_LVM2 is not set +# BR2_PACKAGE_MBPFAN is not set +# BR2_PACKAGE_MDADM is not set +# BR2_PACKAGE_MDEVD is not set +# BR2_PACKAGE_MEMTESTER is not set +# BR2_PACKAGE_MEMTOOL is not set +# BR2_PACKAGE_MINICOM is not set +# BR2_PACKAGE_NANOCOM is not set +# BR2_PACKAGE_NEARD is not set +# BR2_PACKAGE_NVME is not set +# BR2_PACKAGE_OFONO is not set +# BR2_PACKAGE_OPEN2300 is not set +# BR2_PACKAGE_OPENFPGALOADER is not set +# BR2_PACKAGE_OPENIPMI is not set +# BR2_PACKAGE_OPENOCD is not set +# BR2_PACKAGE_PARTED is not set +# BR2_PACKAGE_PCIUTILS is not set +# BR2_PACKAGE_PDBG is not set +# BR2_PACKAGE_PICOCOM is not set +# BR2_PACKAGE_POWERTOP is not set +# BR2_PACKAGE_PPS_TOOLS is not set +# BR2_PACKAGE_RASPI_GPIO is not set +# BR2_PACKAGE_READ_EDID is not set +# BR2_PACKAGE_RNG_TOOLS is not set +# BR2_PACKAGE_RS485CONF is not set +# BR2_PACKAGE_RTC_TOOLS is not set + +# +# rtl8188eu needs a Linux kernel to be built +# + +# +# rtl8189fs needs a Linux kernel to be built +# + +# +# rtl8723bs needs a Linux kernel to be built +# + +# +# rtl8723bu needs a Linux kernel to be built +# + +# +# rtl8821au needs a Linux kernel to be built +# +# BR2_PACKAGE_SANE_BACKENDS is not set +# BR2_PACKAGE_SDPARM is not set +# BR2_PACKAGE_SETSERIAL is not set +# BR2_PACKAGE_SG3_UTILS is not set +# BR2_PACKAGE_SIGROK_CLI is not set +# BR2_PACKAGE_SISPMCTL is not set +# BR2_PACKAGE_SMARTMONTOOLS is not set +# BR2_PACKAGE_SMSTOOLS3 is not set +# BR2_PACKAGE_SPI_TOOLS is not set +# BR2_PACKAGE_SREDIRD is not set +# BR2_PACKAGE_STATSERIAL is not set +# BR2_PACKAGE_STM32FLASH is not set +# BR2_PACKAGE_SYSSTAT is not set +# BR2_PACKAGE_TARGETCLI_FB is not set + +# +# ti-sgx-libgbm needs udev and a toolchain w/ threads +# + +# +# ti-sgx-um needs the ti-sgx-km driver +# + +# +# ti-sgx-um needs udev and a glibc toolchain w/ threads +# +# BR2_PACKAGE_TI_UIM is not set +# BR2_PACKAGE_TI_UTILS is not set +# BR2_PACKAGE_TIO is not set +# BR2_PACKAGE_TRIGGERHAPPY is not set +# BR2_PACKAGE_UBOOT_TOOLS is not set +# BR2_PACKAGE_UBUS is not set + +# +# uccp420wlan needs a Linux kernel >= 4.2 to be built +# + +# +# udisks needs udev /dev management +# +# BR2_PACKAGE_UHUBCTL is not set +# BR2_PACKAGE_UMTPRD is not set + +# +# upower needs udev /dev management +# +# BR2_PACKAGE_USB_MODESWITCH is not set +# BR2_PACKAGE_USB_MODESWITCH_DATA is not set + +# +# usbmount requires udev to be enabled +# + +# +# usbutils needs udev /dev management and toolchain w/ threads +# +# BR2_PACKAGE_W_SCAN is not set +# BR2_PACKAGE_WIPE is not set +# BR2_PACKAGE_XORRISO is not set + +# +# xr819-xradio driver needs a Linux kernel to be built +# + +# +# Interpreter languages and scripting +# +# BR2_PACKAGE_4TH is not set +# BR2_PACKAGE_ENSCRIPT is not set +BR2_PACKAGE_HOST_ERLANG_ARCH_SUPPORTS=y +# BR2_PACKAGE_EXECLINE is not set +# BR2_PACKAGE_FICL is not set +# BR2_PACKAGE_HASERL is not set +# BR2_PACKAGE_JIMTCL is not set +BR2_PACKAGE_LUA=y +BR2_PACKAGE_PROVIDES_LUAINTERPRETER="lua" +# BR2_PACKAGE_LUA_5_1 is not set +BR2_PACKAGE_LUA_5_3=y +# BR2_PACKAGE_LUA_32BITS is not set +# BR2_PACKAGE_LUA_EDITING_NONE is not set +BR2_PACKAGE_LUA_READLINE=y +# BR2_PACKAGE_LUA_LINENOISE is not set +BR2_PACKAGE_HAS_LUAINTERPRETER=y +BR2_PACKAGE_LUAINTERPRETER_ABI_VERSION="5.3" +BR2_PACKAGE_LUAINTERPRETER_ABI_VERSION_5_3=y +BR2_PACKAGE_PROVIDES_HOST_LUAINTERPRETER="host-lua" + +# +# Lua libraries/modules +# +# BR2_PACKAGE_ARGPARSE is not set +# BR2_PACKAGE_CGILUA is not set +# BR2_PACKAGE_COPAS is not set +# BR2_PACKAGE_COXPCALL is not set +# BR2_PACKAGE_DADO is not set +# BR2_PACKAGE_EASYDBUS is not set +# BR2_PACKAGE_LBASE64 is not set + +# +# ljlinenoise needs LuaJIT +# + +# +# ljsyscall needs LuaJIT +# +# BR2_PACKAGE_LPEG is not set +# BR2_PACKAGE_LPTY is not set +# BR2_PACKAGE_LRANDOM is not set +BR2_PACKAGE_LSQLITE3=y +# BR2_PACKAGE_LUA_BASEXX is not set +# BR2_PACKAGE_LUA_BINARYHEAP is not set +# BR2_PACKAGE_LUA_BIT32 is not set +# BR2_PACKAGE_LUA_CJSON is not set +# BR2_PACKAGE_LUA_COAT is not set +# BR2_PACKAGE_LUA_COATPERSISTENT is not set +# BR2_PACKAGE_LUA_CODEGEN is not set +# BR2_PACKAGE_LUA_CQUEUES is not set +# BR2_PACKAGE_LUA_CSNAPPY is not set +# BR2_PACKAGE_LUA_CURL is not set +# BR2_PACKAGE_LUA_DATAFILE is not set +# BR2_PACKAGE_LUA_EV is not set +# BR2_PACKAGE_LUA_FIFO is not set +# BR2_PACKAGE_LUA_FLU is not set +# BR2_PACKAGE_LUA_GD is not set +# BR2_PACKAGE_LUA_HTTP is not set +# BR2_PACKAGE_LUA_ICONV is not set +# BR2_PACKAGE_LUA_LIVR is not set +# BR2_PACKAGE_LUA_LIVR_EXTRA is not set +# BR2_PACKAGE_LUA_LPEG_PATTERNS is not set +# BR2_PACKAGE_LUA_LUNITX is not set +# BR2_PACKAGE_LUA_LYAML is not set +# BR2_PACKAGE_LUA_MARKDOWN is not set +# BR2_PACKAGE_LUA_MESSAGEPACK is not set + +# +# lua-msgpack-native needs Lua 5.1 or LuaJIT +# +# BR2_PACKAGE_LUA_PERIPHERY is not set +# BR2_PACKAGE_LUA_RESTY_HTTP is not set +# BR2_PACKAGE_LUA_ROTAS is not set +# BR2_PACKAGE_LUA_SAILOR is not set +# BR2_PACKAGE_LUA_SDL2 is not set +# BR2_PACKAGE_LUA_SILVA is not set +# BR2_PACKAGE_LUA_STD_DEBUG is not set +# BR2_PACKAGE_LUA_STD_NORMALIZE is not set +# BR2_PACKAGE_LUA_STDLIB is not set +# BR2_PACKAGE_LUA_TESTMORE is not set +# BR2_PACKAGE_LUA_UTF8 is not set +# BR2_PACKAGE_LUA_VALUA is not set + +# +# luabitop needs Lua 5.1 +# +# BR2_PACKAGE_LUADBI is not set +# BR2_PACKAGE_LUADBI_SQLITE3 is not set +# BR2_PACKAGE_LUAEXPAT is not set +# BR2_PACKAGE_LUAEXPATUTILS is not set +# BR2_PACKAGE_LUAFILESYSTEM is not set +# BR2_PACKAGE_LUAJSON is not set +# BR2_PACKAGE_LUALOGGING is not set +# BR2_PACKAGE_LUAOSSL is not set +# BR2_PACKAGE_LUAPOSIX is not set +# BR2_PACKAGE_LUASEC is not set +# BR2_PACKAGE_LUASOCKET is not set +# BR2_PACKAGE_LUASQL_SQLITE3 is not set +# BR2_PACKAGE_LUASYSLOG is not set +# BR2_PACKAGE_LUTOK is not set +# BR2_PACKAGE_LUV is not set + +# +# luvi needs LuaJIT +# +# BR2_PACKAGE_LZLIB is not set + +# +# orbit needs a Lua 5.1 interpreter +# +# BR2_PACKAGE_RINGS is not set + +# +# turbolua needs LuaJIT +# +# BR2_PACKAGE_WSAPI is not set +# BR2_PACKAGE_WSAPI_FCGI is not set +# BR2_PACKAGE_WSAPI_XAVANTE is not set +# BR2_PACKAGE_XAVANTE is not set +# BR2_PACKAGE_MICROPYTHON is not set +BR2_PACKAGE_HOST_MONO_ARCH_SUPPORTS=y +BR2_PACKAGE_HOST_OPENJDK_BIN_ARCH_SUPPORTS=y +BR2_PACKAGE_OPENJDK_ARCH_SUPPORTS=y + +# +# openjdk needs X.Org +# +# BR2_PACKAGE_PERL is not set +# BR2_PACKAGE_PHP is not set +# BR2_PACKAGE_PYTHON is not set +BR2_PACKAGE_PYTHON3=y +# BR2_PACKAGE_PYTHON3_PY_ONLY is not set +BR2_PACKAGE_PYTHON3_PYC_ONLY=y +# BR2_PACKAGE_PYTHON3_PY_PYC is not set + +# +# core python3 modules +# + +# +# The following modules are unusual or require extra libraries +# +# BR2_PACKAGE_PYTHON3_BZIP2 is not set +# BR2_PACKAGE_PYTHON3_CODECSCJK is not set +# BR2_PACKAGE_PYTHON3_CURSES is not set +# BR2_PACKAGE_PYTHON3_DECIMAL is not set +# BR2_PACKAGE_PYTHON3_OSSAUDIODEV is not set +# BR2_PACKAGE_PYTHON3_READLINE is not set +# BR2_PACKAGE_PYTHON3_SSL is not set +BR2_PACKAGE_PYTHON3_UNICODEDATA=y +# BR2_PACKAGE_PYTHON3_SQLITE is not set +BR2_PACKAGE_PYTHON3_PYEXPAT=y +# BR2_PACKAGE_PYTHON3_XZ is not set +# BR2_PACKAGE_PYTHON3_ZLIB is not set + +# +# External python modules +# +# BR2_PACKAGE_PYTHON_AENUM is not set +# BR2_PACKAGE_PYTHON_AIOBLESCAN is not set +# BR2_PACKAGE_PYTHON_AIOCOAP is not set +# BR2_PACKAGE_PYTHON_AIOCONSOLE is not set +# BR2_PACKAGE_PYTHON_AIODNS is not set +# BR2_PACKAGE_PYTHON_AIOHTTP is not set +# BR2_PACKAGE_PYTHON_AIOHTTP_CORS is not set +# BR2_PACKAGE_PYTHON_AIOHTTP_DEBUGTOOLBAR is not set +# BR2_PACKAGE_PYTHON_AIOHTTP_JINJA2 is not set +# BR2_PACKAGE_PYTHON_AIOHTTP_MAKO is not set +# BR2_PACKAGE_PYTHON_AIOHTTP_REMOTES is not set +# BR2_PACKAGE_PYTHON_AIOHTTP_SECURITY is not set +# BR2_PACKAGE_PYTHON_AIOHTTP_SESSION is not set +# BR2_PACKAGE_PYTHON_AIOHTTP_SSE is not set +# BR2_PACKAGE_PYTHON_AIOJOBS is not set +# BR2_PACKAGE_PYTHON_AIOLOGSTASH is not set +# BR2_PACKAGE_PYTHON_AIOMONITOR is not set +# BR2_PACKAGE_PYTHON_AIOREDIS is not set +# BR2_PACKAGE_PYTHON_AIORWLOCK is not set +# BR2_PACKAGE_PYTHON_AIOSIGNAL is not set +# BR2_PACKAGE_PYTHON_AIOZIPKIN is not set +# BR2_PACKAGE_PYTHON_ALSAAUDIO is not set +# BR2_PACKAGE_PYTHON_ARGH is not set +# BR2_PACKAGE_PYTHON_ARGON2_CFFI is not set +# BR2_PACKAGE_PYTHON_ARROW is not set +# BR2_PACKAGE_PYTHON_ASGIREF is not set +# BR2_PACKAGE_PYTHON_ASN1CRYPTO is not set +# BR2_PACKAGE_PYTHON_ASYNC_LRU is not set +# BR2_PACKAGE_PYTHON_ASYNC_TIMEOUT is not set +# BR2_PACKAGE_PYTHON_ATTRS is not set +# BR2_PACKAGE_PYTHON_AUTOBAHN is not set +# BR2_PACKAGE_PYTHON_AUTOMAT is not set +# BR2_PACKAGE_PYTHON_AVRO is not set +# BR2_PACKAGE_PYTHON_BABEL is not set +# BR2_PACKAGE_PYTHON_BACKCALL is not set +# BR2_PACKAGE_PYTHON_BCRYPT is not set +# BR2_PACKAGE_PYTHON_BEAUTIFULSOUP4 is not set +# BR2_PACKAGE_PYTHON_BITSTRING is not set +# BR2_PACKAGE_PYTHON_BLUEZERO is not set +# BR2_PACKAGE_PYTHON_BOTTLE is not set +# BR2_PACKAGE_PYTHON_BROTLI is not set +# BR2_PACKAGE_PYTHON_BUNCH is not set +# BR2_PACKAGE_PYTHON_CACHED_PROPERTY is not set +# BR2_PACKAGE_PYTHON_CAN is not set +# BR2_PACKAGE_PYTHON_CANOPEN is not set +# BR2_PACKAGE_PYTHON_CBOR is not set +# BR2_PACKAGE_PYTHON_CBOR2 is not set +# BR2_PACKAGE_PYTHON_CCHARDET is not set +# BR2_PACKAGE_PYTHON_CERTIFI is not set +# BR2_PACKAGE_PYTHON_CFFI is not set +# BR2_PACKAGE_PYTHON_CHANNELS is not set +# BR2_PACKAGE_PYTHON_CHANNELS_REDIS is not set +# BR2_PACKAGE_PYTHON_CHARACTERISTIC is not set +# BR2_PACKAGE_PYTHON_CHARDET is not set +# BR2_PACKAGE_PYTHON_CHEETAH is not set +# BR2_PACKAGE_PYTHON_CHEROOT is not set +# BR2_PACKAGE_PYTHON_CHERRYPY is not set +# BR2_PACKAGE_PYTHON_CLICK is not set +# BR2_PACKAGE_PYTHON_COLORAMA is not set +# BR2_PACKAGE_PYTHON_COLORLOG is not set +# BR2_PACKAGE_PYTHON_COLORZERO is not set +# BR2_PACKAGE_PYTHON_CONFIGSHELL_FB is not set +# BR2_PACKAGE_PYTHON_CONSTANTLY is not set +# BR2_PACKAGE_PYTHON_COUCHDB is not set +# BR2_PACKAGE_PYTHON_CRCMOD is not set +# BR2_PACKAGE_PYTHON_CRONTAB is not set +# BR2_PACKAGE_PYTHON_CROSSBAR is not set +# BR2_PACKAGE_PYTHON_CRYPTOGRAPHY is not set +# BR2_PACKAGE_PYTHON_CSSSELECT is not set +# BR2_PACKAGE_PYTHON_CSSUTILS is not set +# BR2_PACKAGE_PYTHON_CYCLER is not set +# BR2_PACKAGE_PYTHON_DAEMON is not set +# BR2_PACKAGE_PYTHON_DAEMONIZE is not set +# BR2_PACKAGE_PYTHON_DAPHNE is not set +# BR2_PACKAGE_PYTHON_DATAPROPERTY is not set +# BR2_PACKAGE_PYTHON_DATEUTIL is not set +# BR2_PACKAGE_PYTHON_DECORATOR is not set +# BR2_PACKAGE_PYTHON_DIALOG3 is not set +# BR2_PACKAGE_PYTHON_DICTTOXML is not set +# BR2_PACKAGE_PYTHON_DJANGO is not set +# BR2_PACKAGE_PYTHON_DJANGO_ENUMFIELDS is not set +# BR2_PACKAGE_PYTHON_DNSPYTHON is not set +# BR2_PACKAGE_PYTHON_DOCKER is not set +# BR2_PACKAGE_PYTHON_DOCKER_PYCREDS is not set +# BR2_PACKAGE_PYTHON_DOCKERPTY is not set +# BR2_PACKAGE_PYTHON_DOCOPT is not set +# BR2_PACKAGE_PYTHON_DOCUTILS is not set +# BR2_PACKAGE_PYTHON_DOMINATE is not set +# BR2_PACKAGE_PYTHON_DPKT is not set +# BR2_PACKAGE_PYTHON_ECDSA is not set +# BR2_PACKAGE_PYTHON_ENGINEIO is not set +# BR2_PACKAGE_PYTHON_ENTRYPOINTS is not set +# BR2_PACKAGE_PYTHON_ESPTOOL is not set +# BR2_PACKAGE_PYTHON_FALCON is not set +# BR2_PACKAGE_PYTHON_FILELOCK is not set +# BR2_PACKAGE_PYTHON_FIRE is not set +# BR2_PACKAGE_PYTHON_FLASK is not set +# BR2_PACKAGE_PYTHON_FLASK_BABEL is not set +# BR2_PACKAGE_PYTHON_FLASK_CORS is not set +# BR2_PACKAGE_PYTHON_FLASK_JSONRPC is not set +# BR2_PACKAGE_PYTHON_FLASK_LOGIN is not set +# BR2_PACKAGE_PYTHON_FLASK_SQLALCHEMY is not set +# BR2_PACKAGE_PYTHON_FLATBUFFERS is not set +# BR2_PACKAGE_PYTHON_FROZENLIST is not set +# BR2_PACKAGE_PYTHON_FUTURE is not set +# BR2_PACKAGE_PYTHON_GITDB2 is not set +# BR2_PACKAGE_PYTHON_GOBJECT is not set +BR2_PACKAGE_PYTHON_GREENLET_ARCH_SUPPORTS=y +# BR2_PACKAGE_PYTHON_GREENLET is not set +# BR2_PACKAGE_PYTHON_GUNICORN is not set +# BR2_PACKAGE_PYTHON_H2 is not set +# BR2_PACKAGE_PYTHON_HIREDIS is not set +# BR2_PACKAGE_PYTHON_HPACK is not set +# BR2_PACKAGE_PYTHON_HTML5LIB is not set +# BR2_PACKAGE_PYTHON_HTTPLIB2 is not set +# BR2_PACKAGE_PYTHON_HUEPY is not set +# BR2_PACKAGE_PYTHON_HUMANIZE is not set +# BR2_PACKAGE_PYTHON_HYPERFRAME is not set +# BR2_PACKAGE_PYTHON_HYPERLINK is not set +# BR2_PACKAGE_PYTHON_IBMIOTF is not set +# BR2_PACKAGE_PYTHON_IDNA is not set +# BR2_PACKAGE_PYTHON_IFADDR is not set +# BR2_PACKAGE_PYTHON_INCREMENTAL is not set +# BR2_PACKAGE_PYTHON_INFLECTION is not set +# BR2_PACKAGE_PYTHON_INFLUXDB is not set +# BR2_PACKAGE_PYTHON_INIPARSE is not set +# BR2_PACKAGE_PYTHON_IOWAIT is not set +# BR2_PACKAGE_PYTHON_IPTABLES is not set +# BR2_PACKAGE_PYTHON_IPYTHON is not set +# BR2_PACKAGE_PYTHON_IPYTHON_GENUTILS is not set +# BR2_PACKAGE_PYTHON_ISO8601 is not set +# BR2_PACKAGE_PYTHON_ITSDANGEROUS is not set +# BR2_PACKAGE_PYTHON_JANUS is not set +# BR2_PACKAGE_PYTHON_JARACO_CLASSES is not set +# BR2_PACKAGE_PYTHON_JARACO_FUNCTOOLS is not set +# BR2_PACKAGE_PYTHON_JEDI is not set +# BR2_PACKAGE_PYTHON_JINJA2 is not set +# BR2_PACKAGE_PYTHON_JSON_SCHEMA_VALIDATOR is not set +# BR2_PACKAGE_PYTHON_JSONMODELS is not set +# BR2_PACKAGE_PYTHON_JSONSCHEMA is not set +# BR2_PACKAGE_PYTHON_KEYRING is not set +# BR2_PACKAGE_PYTHON_KIWISOLVER is not set +# BR2_PACKAGE_PYTHON_LIBCONFIG is not set +# BR2_PACKAGE_PYTHON_LIBUSB1 is not set +# BR2_PACKAGE_PYTHON_LMDB is not set +# BR2_PACKAGE_PYTHON_LOCKFILE is not set +# BR2_PACKAGE_PYTHON_LOGBOOK is not set +# BR2_PACKAGE_PYTHON_LOGSTASH is not set +# BR2_PACKAGE_PYTHON_LXML is not set +# BR2_PACKAGE_PYTHON_M2R is not set +# BR2_PACKAGE_PYTHON_MAKO is not set +# BR2_PACKAGE_PYTHON_MARKDOWN is not set +# BR2_PACKAGE_PYTHON_MARKDOWN2 is not set +# BR2_PACKAGE_PYTHON_MARKUPSAFE is not set +# BR2_PACKAGE_PYTHON_MBSTRDECODER is not set +# BR2_PACKAGE_PYTHON_MELD3 is not set +# BR2_PACKAGE_PYTHON_MIMEPARSE is not set +# BR2_PACKAGE_PYTHON_MISTUNE is not set +# BR2_PACKAGE_PYTHON_MODBUS_TK is not set +# BR2_PACKAGE_PYTHON_MORE_ITERTOOLS is not set +# BR2_PACKAGE_PYTHON_MSGFY is not set +# BR2_PACKAGE_PYTHON_MSGPACK is not set +# BR2_PACKAGE_PYTHON_MULTIDICT is not set +# BR2_PACKAGE_PYTHON_MUTAGEN is not set +# BR2_PACKAGE_PYTHON_MWCLIENT is not set +# BR2_PACKAGE_PYTHON_MWSCRAPE2SLOB is not set +# BR2_PACKAGE_PYTHON_NESTED_DICT is not set +# BR2_PACKAGE_PYTHON_NETADDR is not set +# BR2_PACKAGE_PYTHON_NETIFACES is not set +# BR2_PACKAGE_PYTHON_NETWORKX is not set +# BR2_PACKAGE_PYTHON_OAUTHLIB is not set +# BR2_PACKAGE_PYTHON_PAHO_MQTT is not set +# BR2_PACKAGE_PYTHON_PARAMIKO is not set +# BR2_PACKAGE_PYTHON_PARSO is not set +# BR2_PACKAGE_PYTHON_PASSLIB is not set +# BR2_PACKAGE_PYTHON_PATHPY is not set +# BR2_PACKAGE_PYTHON_PATHTOOLS is not set +# BR2_PACKAGE_PYTHON_PATHVALIDATE is not set +# BR2_PACKAGE_PYTHON_PERIPHERY is not set +# BR2_PACKAGE_PYTHON_PEXPECT is not set +# BR2_PACKAGE_PYTHON_PICKLESHARE is not set +# BR2_PACKAGE_PYTHON_PIGPIO is not set +# BR2_PACKAGE_PYTHON_PILLOW is not set +# BR2_PACKAGE_PYTHON_PIP is not set +# BR2_PACKAGE_PYTHON_PLY is not set +# BR2_PACKAGE_PYTHON_PORTEND is not set +# BR2_PACKAGE_PYTHON_POSIX_IPC is not set +# BR2_PACKAGE_PYTHON_PRIORITY is not set +# BR2_PACKAGE_PYTHON_PROMPT_TOOLKIT is not set +# BR2_PACKAGE_PYTHON_PROTOBUF is not set +# BR2_PACKAGE_PYTHON_PSUTIL is not set +# BR2_PACKAGE_PYTHON_PSYCOPG2 is not set +# BR2_PACKAGE_PYTHON_PTYPROCESS is not set +# BR2_PACKAGE_PYTHON_PUDB is not set +# BR2_PACKAGE_PYTHON_PY is not set +# BR2_PACKAGE_PYTHON_PYAES is not set +# BR2_PACKAGE_PYTHON_PYALSA is not set +# BR2_PACKAGE_PYTHON_PYASN1 is not set +# BR2_PACKAGE_PYTHON_PYASN1_MODULES is not set +# BR2_PACKAGE_PYTHON_PYBIND is not set +# BR2_PACKAGE_PYTHON_PYCAIRO is not set +# BR2_PACKAGE_PYTHON_PYCARES is not set +# BR2_PACKAGE_PYTHON_PYCLI is not set +# BR2_PACKAGE_PYTHON_PYCPARSER is not set +# BR2_PACKAGE_PYTHON_PYCRYPTODOMEX is not set +# BR2_PACKAGE_PYTHON_PYDAL is not set +# BR2_PACKAGE_PYTHON_PYDANTIC is not set +# BR2_PACKAGE_PYTHON_PYELFTOOLS is not set +# BR2_PACKAGE_PYTHON_PYFTPDLIB is not set +# BR2_PACKAGE_PYTHON_PYGAME is not set +# BR2_PACKAGE_PYTHON_PYGMENTS is not set +# BR2_PACKAGE_PYTHON_PYHAMCREST is not set +# BR2_PACKAGE_PYTHON_PYICU is not set +# BR2_PACKAGE_PYTHON_PYINOTIFY is not set +# BR2_PACKAGE_PYTHON_PYJWT is not set +# BR2_PACKAGE_PYTHON_PYLIBFTDI is not set +# BR2_PACKAGE_PYTHON_PYLRU is not set +# BR2_PACKAGE_PYTHON_PYMODBUS is not set +# BR2_PACKAGE_PYTHON_PYMYSQL is not set +# BR2_PACKAGE_PYTHON_PYNACL is not set +# BR2_PACKAGE_PYTHON_PYOPENSSL is not set +# BR2_PACKAGE_PYTHON_PYPARSING is not set +# BR2_PACKAGE_PYTHON_PYPARTED is not set +# BR2_PACKAGE_PYTHON_PYQRCODE is not set + +# +# python-pyqt5 needs Qt5 +# +# BR2_PACKAGE_PYTHON_PYRATEMP is not set +# BR2_PACKAGE_PYTHON_PYROUTE2 is not set +# BR2_PACKAGE_PYTHON_PYSENDFILE is not set +# BR2_PACKAGE_PYTHON_PYSFTP is not set +# BR2_PACKAGE_PYTHON_PYSMB is not set +# BR2_PACKAGE_PYTHON_PYSMI is not set +# BR2_PACKAGE_PYTHON_PYSNMP is not set +# BR2_PACKAGE_PYTHON_PYSNMP_MIBS is not set +# BR2_PACKAGE_PYTHON_PYSOCKS is not set +# BR2_PACKAGE_PYTHON_PYTABLEREADER is not set +# BR2_PACKAGE_PYTHON_PYTABLEWRITER is not set +# BR2_PACKAGE_PYTHON_PYTRIE is not set +# BR2_PACKAGE_PYTHON_PYTZ is not set + +# +# python-pyudev needs udev /dev management +# +# BR2_PACKAGE_PYTHON_PYUSB is not set +# BR2_PACKAGE_PYTHON_PYXB is not set +# BR2_PACKAGE_PYTHON_PYYAML is not set +# BR2_PACKAGE_PYTHON_PYZMQ is not set +# BR2_PACKAGE_PYTHON_RAVEN is not set +# BR2_PACKAGE_PYTHON_REDIS is not set +# BR2_PACKAGE_PYTHON_REENTRY is not set +# BR2_PACKAGE_PYTHON_REGEX is not set +# BR2_PACKAGE_PYTHON_REMI is not set +# BR2_PACKAGE_PYTHON_REQUEST_ID is not set +# BR2_PACKAGE_PYTHON_REQUESTS is not set +# BR2_PACKAGE_PYTHON_REQUESTS_OAUTHLIB is not set +# BR2_PACKAGE_PYTHON_REQUESTS_TOOLBELT is not set +# BR2_PACKAGE_PYTHON_RPI_WS281X is not set +# BR2_PACKAGE_PYTHON_RTSLIB_FB is not set +# BR2_PACKAGE_PYTHON_SCANDIR is not set +# BR2_PACKAGE_PYTHON_SCAPY is not set +# BR2_PACKAGE_PYTHON_SCHEDULE is not set +# BR2_PACKAGE_PYTHON_SDNOTIFY is not set +# BR2_PACKAGE_PYTHON_SECRETSTORAGE is not set +# BR2_PACKAGE_PYTHON_SEE is not set +# BR2_PACKAGE_PYTHON_SEMVER is not set +# BR2_PACKAGE_PYTHON_SENTRY_SDK is not set +# BR2_PACKAGE_PYTHON_SERIAL is not set +# BR2_PACKAGE_PYTHON_SERIAL_ASYNCIO is not set +# BR2_PACKAGE_PYTHON_SERVICE_IDENTITY is not set +# BR2_PACKAGE_PYTHON_SETPROCTITLE is not set +# BR2_PACKAGE_PYTHON_SETUPTOOLS is not set +# BR2_PACKAGE_PYTHON_SH is not set +# BR2_PACKAGE_PYTHON_SHUTILWHICH is not set +# BR2_PACKAGE_PYTHON_SIMPLEAUDIO is not set +# BR2_PACKAGE_PYTHON_SIMPLEGENERIC is not set +# BR2_PACKAGE_PYTHON_SIMPLEJSON is not set +# BR2_PACKAGE_PYTHON_SIMPLELOGGING is not set +# BR2_PACKAGE_PYTHON_SIMPLESQLITE is not set +# BR2_PACKAGE_PYTHON_SIX is not set +# BR2_PACKAGE_PYTHON_SLOB is not set +# BR2_PACKAGE_PYTHON_SMBUS_CFFI is not set +# BR2_PACKAGE_PYTHON_SMMAP2 is not set +# BR2_PACKAGE_PYTHON_SNAPPY is not set +# BR2_PACKAGE_PYTHON_SOCKETIO is not set +# BR2_PACKAGE_PYTHON_SOCKJS is not set +# BR2_PACKAGE_PYTHON_SORTEDCONTAINERS is not set +# BR2_PACKAGE_PYTHON_SOUPSIEVE is not set +# BR2_PACKAGE_PYTHON_SPIDEV is not set +# BR2_PACKAGE_PYTHON_SQLALCHEMY is not set +# BR2_PACKAGE_PYTHON_SQLITESCHEMA is not set +# BR2_PACKAGE_PYTHON_SQLPARSE is not set + +# +# python-systemd needs systemd +# +# BR2_PACKAGE_PYTHON_TABLEDATA is not set +# BR2_PACKAGE_PYTHON_TEMPORA is not set +# BR2_PACKAGE_PYTHON_TERMCOLOR is not set +# BR2_PACKAGE_PYTHON_TERMINALTABLES is not set +# BR2_PACKAGE_PYTHON_TEXTTABLE is not set +# BR2_PACKAGE_PYTHON_THRIFT is not set +# BR2_PACKAGE_PYTHON_TINYRPC is not set +# BR2_PACKAGE_PYTHON_TOMAKO is not set +# BR2_PACKAGE_PYTHON_TOML is not set +# BR2_PACKAGE_PYTHON_TORNADO is not set +# BR2_PACKAGE_PYTHON_TQDM is not set +# BR2_PACKAGE_PYTHON_TRAITLETS is not set +# BR2_PACKAGE_PYTHON_TREQ is not set +# BR2_PACKAGE_PYTHON_TWISTED is not set +# BR2_PACKAGE_PYTHON_TXAIO is not set +# BR2_PACKAGE_PYTHON_TXDBUS is not set +# BR2_PACKAGE_PYTHON_TXTORCON is not set +# BR2_PACKAGE_PYTHON_TYPEPY is not set +# BR2_PACKAGE_PYTHON_U_MSGPACK is not set +# BR2_PACKAGE_PYTHON_UBJSON is not set +# BR2_PACKAGE_PYTHON_UJSON is not set +# BR2_PACKAGE_PYTHON_URLLIB3 is not set +# BR2_PACKAGE_PYTHON_URWID is not set +# BR2_PACKAGE_PYTHON_UVLOOP is not set +# BR2_PACKAGE_PYTHON_VALIDATORS is not set +# BR2_PACKAGE_PYTHON_VERSIONTOOLS is not set +# BR2_PACKAGE_PYTHON_VISITOR is not set +# BR2_PACKAGE_PYTHON_WATCHDOG is not set +# BR2_PACKAGE_PYTHON_WCWIDTH is not set +# BR2_PACKAGE_PYTHON_WEB2PY is not set +# BR2_PACKAGE_PYTHON_WEBENCODINGS is not set +# BR2_PACKAGE_PYTHON_WEBOB is not set +# BR2_PACKAGE_PYTHON_WEBPY is not set +# BR2_PACKAGE_PYTHON_WEBSOCKET_CLIENT is not set +# BR2_PACKAGE_PYTHON_WEBSOCKETS is not set +# BR2_PACKAGE_PYTHON_WERKZEUG is not set +# BR2_PACKAGE_PYTHON_WHOOSH is not set +# BR2_PACKAGE_PYTHON_WRAPT is not set +# BR2_PACKAGE_PYTHON_WS4PY is not set +# BR2_PACKAGE_PYTHON_WSACCEL is not set +# BR2_PACKAGE_PYTHON_WTFORMS is not set +# BR2_PACKAGE_PYTHON_XLIB is not set +# BR2_PACKAGE_PYTHON_XLRD is not set +# BR2_PACKAGE_PYTHON_XLSXWRITER is not set +# BR2_PACKAGE_PYTHON_XLUTILS is not set +# BR2_PACKAGE_PYTHON_XLWT is not set +# BR2_PACKAGE_PYTHON_XMLTODICT is not set +# BR2_PACKAGE_PYTHON_YARL is not set +# BR2_PACKAGE_PYTHON_YATL is not set +# BR2_PACKAGE_PYTHON_ZC_LOCKFILE is not set +# BR2_PACKAGE_PYTHON_ZEROCONF is not set +# BR2_PACKAGE_PYTHON_ZOPE_INTERFACE is not set +# BR2_PACKAGE_QJS is not set +# BR2_PACKAGE_RUBY is not set +# BR2_PACKAGE_TCL is not set + +# +# Libraries +# + +# +# Audio/Sound +# +# BR2_PACKAGE_ALSA_LIB is not set +# BR2_PACKAGE_ALURE is not set +# BR2_PACKAGE_AUBIO is not set +# BR2_PACKAGE_AUDIOFILE is not set +# BR2_PACKAGE_BCG729 is not set +# BR2_PACKAGE_CAPS is not set +# BR2_PACKAGE_LIBAO is not set +# BR2_PACKAGE_LIBASPLIB is not set +# BR2_PACKAGE_LIBBROADVOICE is not set +# BR2_PACKAGE_LIBCDAUDIO is not set +# BR2_PACKAGE_LIBCDDB is not set +# BR2_PACKAGE_LIBCDIO is not set +# BR2_PACKAGE_LIBCDIO_PARANOIA is not set +# BR2_PACKAGE_LIBCODEC2 is not set +# BR2_PACKAGE_LIBCUE is not set +# BR2_PACKAGE_LIBCUEFILE is not set +# BR2_PACKAGE_LIBEBUR128 is not set +# BR2_PACKAGE_LIBG7221 is not set +# BR2_PACKAGE_LIBGSM is not set +# BR2_PACKAGE_LIBID3TAG is not set +# BR2_PACKAGE_LIBILBC is not set +# BR2_PACKAGE_LIBLO is not set +# BR2_PACKAGE_LIBMAD is not set +# BR2_PACKAGE_LIBMODPLUG is not set +# BR2_PACKAGE_LIBMPD is not set +# BR2_PACKAGE_LIBMPDCLIENT is not set +# BR2_PACKAGE_LIBREPLAYGAIN is not set +# BR2_PACKAGE_LIBSAMPLERATE is not set +# BR2_PACKAGE_LIBSIDPLAY2 is not set +# BR2_PACKAGE_LIBSILK is not set +# BR2_PACKAGE_LIBSNDFILE is not set +# BR2_PACKAGE_LIBSOUNDTOUCH is not set +# BR2_PACKAGE_LIBSOXR is not set +# BR2_PACKAGE_LIBVORBIS is not set +# BR2_PACKAGE_MP4V2 is not set +BR2_PACKAGE_OPENAL_ARCH_SUPPORTS=y +# BR2_PACKAGE_OPENAL is not set +# BR2_PACKAGE_OPENCORE_AMR is not set +# BR2_PACKAGE_OPUS is not set +# BR2_PACKAGE_OPUSFILE is not set +# BR2_PACKAGE_PORTAUDIO is not set +# BR2_PACKAGE_SBC is not set +# BR2_PACKAGE_SPANDSP is not set +# BR2_PACKAGE_SPEEX is not set +# BR2_PACKAGE_SPEEXDSP is not set +# BR2_PACKAGE_TAGLIB is not set +# BR2_PACKAGE_TINYALSA is not set +# BR2_PACKAGE_TREMOR is not set +# BR2_PACKAGE_VO_AACENC is not set + +# +# Compression and decompression +# +# BR2_PACKAGE_LIBARCHIVE is not set +# BR2_PACKAGE_LIBMSPACK is not set +# BR2_PACKAGE_LIBSQUISH is not set +# BR2_PACKAGE_LIBZIP is not set +BR2_PACKAGE_LZ4=y +# BR2_PACKAGE_LZ4_PROGS is not set +BR2_PACKAGE_LZO=y +# BR2_PACKAGE_MINIZIP is not set +# BR2_PACKAGE_SNAPPY is not set +# BR2_PACKAGE_SZIP is not set +BR2_PACKAGE_ZLIB=y +BR2_PACKAGE_LIBZLIB=y +BR2_PACKAGE_HAS_ZLIB=y +BR2_PACKAGE_PROVIDES_ZLIB="libzlib" +BR2_PACKAGE_PROVIDES_HOST_ZLIB="host-libzlib" +# BR2_PACKAGE_ZZIPLIB is not set + +# +# Crypto +# +# BR2_PACKAGE_BEARSSL is not set +# BR2_PACKAGE_BEECRYPT is not set +# BR2_PACKAGE_CA_CERTIFICATES is not set + +# +# cryptodev needs a Linux kernel to be built +# +# BR2_PACKAGE_GCR is not set +# BR2_PACKAGE_GNUTLS is not set +# BR2_PACKAGE_LIBARGON2 is not set +BR2_PACKAGE_LIBASSUAN=y +BR2_PACKAGE_LIBGCRYPT=y +BR2_PACKAGE_LIBGPG_ERROR_ARCH_SUPPORTS=y +BR2_PACKAGE_LIBGPG_ERROR=y +BR2_PACKAGE_LIBGPG_ERROR_SYSCFG="riscv64-unknown-linux-gnu" +# BR2_PACKAGE_LIBGPGME is not set +# BR2_PACKAGE_LIBKCAPI is not set +BR2_PACKAGE_LIBKSBA=y +# BR2_PACKAGE_LIBMCRYPT is not set +# BR2_PACKAGE_LIBMHASH is not set +# BR2_PACKAGE_LIBNSS is not set +# BR2_PACKAGE_LIBOLM is not set +# BR2_PACKAGE_LIBP11 is not set +# BR2_PACKAGE_LIBSCRYPT is not set +# BR2_PACKAGE_LIBSECRET is not set +# BR2_PACKAGE_LIBSHA1 is not set +# BR2_PACKAGE_LIBSODIUM is not set +# BR2_PACKAGE_LIBSSH is not set +# BR2_PACKAGE_LIBSSH2 is not set +# BR2_PACKAGE_LIBTOMCRYPT is not set +# BR2_PACKAGE_LIBUECC is not set +# BR2_PACKAGE_MBEDTLS is not set +# BR2_PACKAGE_NETTLE is not set +# BR2_PACKAGE_OPENSSL is not set +BR2_PACKAGE_PROVIDES_HOST_OPENSSL="host-libopenssl" +# BR2_PACKAGE_PKCS11_HELPER is not set +# BR2_PACKAGE_RHASH is not set +# BR2_PACKAGE_TINYDTLS is not set +# BR2_PACKAGE_TPM2_TSS is not set +# BR2_PACKAGE_TROUSERS is not set +# BR2_PACKAGE_USTREAM_SSL is not set +# BR2_PACKAGE_WOLFSSL is not set + +# +# Database +# +# BR2_PACKAGE_BERKELEYDB is not set +# BR2_PACKAGE_CPPDB is not set +# BR2_PACKAGE_GDBM is not set +# BR2_PACKAGE_HIREDIS is not set +# BR2_PACKAGE_KOMPEXSQLITE is not set +# BR2_PACKAGE_LEVELDB is not set +# BR2_PACKAGE_LIBGIT2 is not set +# BR2_PACKAGE_LIBODB is not set +# BR2_PACKAGE_MYSQL is not set +# BR2_PACKAGE_POSTGRESQL is not set +# BR2_PACKAGE_REDIS is not set +# BR2_PACKAGE_ROCKSDB is not set + +# +# sqlcipher conflicts with sqlite +# +BR2_PACKAGE_SQLITE=y +# BR2_PACKAGE_SQLITE_STAT4 is not set +# BR2_PACKAGE_SQLITE_ENABLE_COLUMN_METADATA is not set +# BR2_PACKAGE_SQLITE_ENABLE_FTS3 is not set +# BR2_PACKAGE_SQLITE_ENABLE_JSON1 is not set +# BR2_PACKAGE_SQLITE_ENABLE_UNLOCK_NOTIFY is not set +# BR2_PACKAGE_SQLITE_SECURE_DELETE is not set +# BR2_PACKAGE_SQLITE_NO_SYNC is not set +# BR2_PACKAGE_UNIXODBC is not set + +# +# Filesystem +# +# BR2_PACKAGE_GAMIN is not set +# BR2_PACKAGE_LIBCONFIG is not set +# BR2_PACKAGE_LIBCONFUSE is not set +# BR2_PACKAGE_LIBFUSE is not set +# BR2_PACKAGE_LIBFUSE3 is not set +# BR2_PACKAGE_LIBLOCKFILE is not set +# BR2_PACKAGE_LIBNFS is not set +# BR2_PACKAGE_LIBSYSFS is not set +# BR2_PACKAGE_LOCKDEV is not set +# BR2_PACKAGE_PHYSFS is not set + +# +# Graphics +# +# BR2_PACKAGE_ASSIMP is not set + +# +# at-spi2-atk depends on X.org +# + +# +# at-spi2-core depends on X.org +# +# BR2_PACKAGE_ATK is not set +# BR2_PACKAGE_ATKMM is not set +# BR2_PACKAGE_BULLET is not set +# BR2_PACKAGE_CAIRO is not set +# BR2_PACKAGE_CAIROMM is not set + +# +# chipmunk needs an OpenGL backend +# +# BR2_PACKAGE_EXEMPI is not set +# BR2_PACKAGE_EXIV2 is not set +# BR2_PACKAGE_FONTCONFIG is not set +# BR2_PACKAGE_FREETYPE is not set +# BR2_PACKAGE_GD is not set +# BR2_PACKAGE_GDK_PIXBUF is not set +# BR2_PACKAGE_GIFLIB is not set + +# +# granite needs libgtk3 and a toolchain w/ wchar, threads +# +# BR2_PACKAGE_GRAPHITE2 is not set + +# +# gtkmm3 needs libgtk3 and a toolchain w/ C++, wchar, threads, gcc >= 4.9 +# +# BR2_PACKAGE_HARFBUZZ is not set +# BR2_PACKAGE_IJS is not set +# BR2_PACKAGE_IMLIB2 is not set + +# +# irrlicht needs X11 and an OpenGL provider +# +# BR2_PACKAGE_JASPER is not set +# BR2_PACKAGE_JBIG2DEC is not set +# BR2_PACKAGE_JPEG is not set +# BR2_PACKAGE_KMSXX is not set +# BR2_PACKAGE_LCMS2 is not set +# BR2_PACKAGE_LENSFUN is not set +# BR2_PACKAGE_LEPTONICA is not set +# BR2_PACKAGE_LIBART is not set +# BR2_PACKAGE_LIBDMTX is not set +# BR2_PACKAGE_LIBDRM is not set + +# +# libepoxy needs an OpenGL and/or OpenGL EGL backend +# +# BR2_PACKAGE_LIBEXIF is not set + +# +# libfm needs X.org and a toolchain w/ wchar, threads, C++, gcc >= 4.8 +# +# BR2_PACKAGE_LIBFM_EXTRA is not set + +# +# libfreeglut depends on X.org and needs an OpenGL backend +# +# BR2_PACKAGE_LIBFREEIMAGE is not set +# BR2_PACKAGE_LIBGEOTIFF is not set + +# +# libglew depends on X.org and needs an OpenGL backend +# + +# +# libglfw depends on X.org and needs an OpenGL backend +# + +# +# libglu needs an OpenGL backend +# +# BR2_PACKAGE_LIBGTA is not set + +# +# libgtk3 needs an OpenGL or an OpenGL-EGL/wayland backend +# +# BR2_PACKAGE_LIBMEDIAART is not set +# BR2_PACKAGE_LIBMNG is not set +# BR2_PACKAGE_LIBPNG is not set +# BR2_PACKAGE_LIBQRENCODE is not set +# BR2_PACKAGE_LIBRAW is not set +# BR2_PACKAGE_LIBRSVG is not set + +# +# libsoil needs an OpenGL backend and a toolchain w/ dynamic library +# +# BR2_PACKAGE_LIBSVG is not set +# BR2_PACKAGE_LIBSVG_CAIRO is not set +# BR2_PACKAGE_LIBSVGTINY is not set +# BR2_PACKAGE_LIBVA is not set +# BR2_PACKAGE_LIBVIPS is not set + +# +# libwpe needs a toolchain w/ C++, dynamic library and an OpenEGL-capable backend +# +# BR2_PACKAGE_MENU_CACHE is not set +# BR2_PACKAGE_OPENCV is not set +# BR2_PACKAGE_OPENCV3 is not set +# BR2_PACKAGE_OPENJPEG is not set +# BR2_PACKAGE_PANGO is not set +# BR2_PACKAGE_PANGOMM is not set + +# +# pipewire needs udev and a toolchain w/ threads +# +# BR2_PACKAGE_PIXMAN is not set +# BR2_PACKAGE_POPPLER is not set +# BR2_PACKAGE_TIFF is not set +# BR2_PACKAGE_WAYLAND is not set +# BR2_PACKAGE_WEBP is not set + +# +# wlroots needs udev, mesa3d w/ EGL and GLES support +# +# BR2_PACKAGE_WOFF2 is not set + +# +# wpebackend-fdo needs a toolchain w/ C++, wchar, threads, dynamic library and an OpenEGL-capable Wayland backend +# +# BR2_PACKAGE_ZBAR is not set +# BR2_PACKAGE_ZXING_CPP is not set + +# +# Hardware handling +# +# BR2_PACKAGE_ACSCCID is not set +# BR2_PACKAGE_C_PERIPHERY is not set +# BR2_PACKAGE_CCID is not set +BR2_PACKAGE_DTC=y +BR2_PACKAGE_DTC_PROGRAMS=y + +# +# dtdiff will not be installed: it requires bash +# +# BR2_PACKAGE_HACKRF is not set + +# +# hidapi needs udev /dev management and a toolchain w/ NPTL threads +# +# BR2_PACKAGE_JITTERENTROPY_LIBRARY is not set +# BR2_PACKAGE_LCDAPI is not set +# BR2_PACKAGE_LET_ME_CREATE is not set +# BR2_PACKAGE_LIBAIO is not set + +# +# libatasmart requires udev to be enabled +# +# BR2_PACKAGE_LIBCEC is not set +# BR2_PACKAGE_LIBFREEFARE is not set +# BR2_PACKAGE_LIBFTDI is not set +# BR2_PACKAGE_LIBFTDI1 is not set +# BR2_PACKAGE_LIBGPHOTO2 is not set +# BR2_PACKAGE_LIBGPIOD is not set + +# +# libgudev needs udev /dev handling and a toolchain w/ wchar, threads +# +# BR2_PACKAGE_LIBHID is not set +# BR2_PACKAGE_LIBIIO is not set + +# +# libinput needs udev /dev management +# +# BR2_PACKAGE_LIBIQRF is not set +# BR2_PACKAGE_LIBLLCP is not set +# BR2_PACKAGE_LIBMBIM is not set +# BR2_PACKAGE_LIBNFC is not set +# BR2_PACKAGE_LIBPCIACCESS is not set +# BR2_PACKAGE_LIBPHIDGET is not set + +# +# libpri needs a kernel to be built +# +# BR2_PACKAGE_LIBQMI is not set +# BR2_PACKAGE_LIBRAW1394 is not set +# BR2_PACKAGE_LIBRTLSDR is not set +# BR2_PACKAGE_LIBSERIAL is not set +# BR2_PACKAGE_LIBSERIALPORT is not set +# BR2_PACKAGE_LIBSIGROK is not set +# BR2_PACKAGE_LIBSIGROKDECODE is not set +# BR2_PACKAGE_LIBSOC is not set + +# +# libss7 needs a kernel to be built +# +# BR2_PACKAGE_LIBUSB is not set +# BR2_PACKAGE_LIBUSBGX is not set +# BR2_PACKAGE_LIBV4L is not set +# BR2_PACKAGE_LIBXKBCOMMON is not set +# BR2_PACKAGE_MTDEV is not set +# BR2_PACKAGE_NEARDAL is not set +# BR2_PACKAGE_OWFS is not set +# BR2_PACKAGE_PCSC_LITE is not set +# BR2_PACKAGE_TSLIB is not set +# BR2_PACKAGE_URG is not set + +# +# Javascript +# +# BR2_PACKAGE_ANGULARJS is not set +# BR2_PACKAGE_BOOTSTRAP is not set +# BR2_PACKAGE_CHARTJS is not set +# BR2_PACKAGE_DUKTAPE is not set +# BR2_PACKAGE_EXPLORERCANVAS is not set +# BR2_PACKAGE_FLOT is not set +# BR2_PACKAGE_JQUERY is not set +# BR2_PACKAGE_JSMIN is not set +# BR2_PACKAGE_JSON_JAVASCRIPT is not set +# BR2_PACKAGE_OPENLAYERS is not set +BR2_PACKAGE_SPIDERMONKEY_ARCH_SUPPORTS=y +# BR2_PACKAGE_SPIDERMONKEY is not set +# BR2_PACKAGE_VUEJS is not set + +# +# JSON/XML +# +# BR2_PACKAGE_BENEJSON is not set +# BR2_PACKAGE_CJSON is not set +BR2_PACKAGE_EXPAT=y +# BR2_PACKAGE_JANSSON is not set +# BR2_PACKAGE_JOSE is not set +# BR2_PACKAGE_JSMN is not set +# BR2_PACKAGE_JSON_C is not set +# BR2_PACKAGE_JSON_FOR_MODERN_CPP is not set +# BR2_PACKAGE_JSON_GLIB is not set +# BR2_PACKAGE_JSONCPP is not set +# BR2_PACKAGE_LIBBSON is not set +# BR2_PACKAGE_LIBFASTJSON is not set +# BR2_PACKAGE_LIBJSON is not set +# BR2_PACKAGE_LIBROXML is not set +# BR2_PACKAGE_LIBUCL is not set +# BR2_PACKAGE_LIBXML2 is not set +# BR2_PACKAGE_LIBXMLPP is not set +# BR2_PACKAGE_LIBXMLRPC is not set +# BR2_PACKAGE_LIBXSLT is not set +# BR2_PACKAGE_LIBYAML is not set +# BR2_PACKAGE_MXML is not set +# BR2_PACKAGE_PUGIXML is not set +# BR2_PACKAGE_RAPIDJSON is not set +# BR2_PACKAGE_RAPIDXML is not set +# BR2_PACKAGE_RAPTOR is not set +# BR2_PACKAGE_TINYXML is not set +# BR2_PACKAGE_TINYXML2 is not set +# BR2_PACKAGE_VALIJSON is not set +# BR2_PACKAGE_XERCES is not set +# BR2_PACKAGE_YAJL is not set +# BR2_PACKAGE_YAML_CPP is not set + +# +# Logging +# +# BR2_PACKAGE_GLOG is not set +# BR2_PACKAGE_LIBLOG4C_LOCALTIME is not set +# BR2_PACKAGE_LIBLOGGING is not set +# BR2_PACKAGE_LOG4CPLUS is not set +# BR2_PACKAGE_LOG4CPP is not set +# BR2_PACKAGE_LOG4CXX is not set +# BR2_PACKAGE_OPENTRACING_CPP is not set +# BR2_PACKAGE_SPDLOG is not set +# BR2_PACKAGE_ZLOG is not set + +# +# Multimedia +# +# BR2_PACKAGE_BITSTREAM is not set +# BR2_PACKAGE_DAV1D is not set +# BR2_PACKAGE_KVAZAAR is not set +# BR2_PACKAGE_LIBAACS is not set +# BR2_PACKAGE_LIBASS is not set +# BR2_PACKAGE_LIBBDPLUS is not set +# BR2_PACKAGE_LIBBLURAY is not set +# BR2_PACKAGE_LIBDCADEC is not set +# BR2_PACKAGE_LIBDVBCSA is not set +# BR2_PACKAGE_LIBDVBPSI is not set +# BR2_PACKAGE_LIBDVBSI is not set +# BR2_PACKAGE_LIBDVDCSS is not set +# BR2_PACKAGE_LIBDVDNAV is not set +# BR2_PACKAGE_LIBDVDREAD is not set +# BR2_PACKAGE_LIBEBML is not set +# BR2_PACKAGE_LIBHDHOMERUN is not set +# BR2_PACKAGE_LIBMATROSKA is not set +# BR2_PACKAGE_LIBMMS is not set +# BR2_PACKAGE_LIBMPEG2 is not set +# BR2_PACKAGE_LIBOGG is not set +# BR2_PACKAGE_LIBOPUSENC is not set +# BR2_PACKAGE_LIBTHEORA is not set +# BR2_PACKAGE_LIBUDFREAD is not set +# BR2_PACKAGE_LIBVPX is not set +# BR2_PACKAGE_LIBYUV is not set +# BR2_PACKAGE_LIVE555 is not set +# BR2_PACKAGE_MEDIASTREAMER is not set +# BR2_PACKAGE_X264 is not set +# BR2_PACKAGE_X265 is not set + +# +# Networking +# +# BR2_PACKAGE_AGENTPP is not set +# BR2_PACKAGE_AZMQ is not set +# BR2_PACKAGE_AZURE_IOT_SDK_C is not set + +# +# batman-adv needs a Linux kernel to be built +# +# BR2_PACKAGE_BELLE_SIP is not set +# BR2_PACKAGE_C_ARES is not set +# BR2_PACKAGE_CGIC is not set +# BR2_PACKAGE_CPPZMQ is not set +# BR2_PACKAGE_CURLPP is not set +# BR2_PACKAGE_CZMQ is not set +# BR2_PACKAGE_DAQ is not set +# BR2_PACKAGE_DAVICI is not set +# BR2_PACKAGE_ENET is not set +# BR2_PACKAGE_FILEMQ is not set +# BR2_PACKAGE_FLICKCURL is not set +# BR2_PACKAGE_FREERADIUS_CLIENT is not set +# BR2_PACKAGE_GENSIO is not set +# BR2_PACKAGE_GEOIP is not set +# BR2_PACKAGE_GLIB_NETWORKING is not set +# BR2_PACKAGE_GRPC is not set +# BR2_PACKAGE_GSSDP is not set +# BR2_PACKAGE_GUPNP is not set +# BR2_PACKAGE_GUPNP_AV is not set +# BR2_PACKAGE_GUPNP_DLNA is not set +# BR2_PACKAGE_IBRCOMMON is not set +# BR2_PACKAGE_IBRDTN is not set +# BR2_PACKAGE_LIBCGI is not set +# BR2_PACKAGE_LIBCGICC is not set +# BR2_PACKAGE_LIBCOAP is not set +# BR2_PACKAGE_LIBCPPRESTSDK is not set +# BR2_PACKAGE_LIBCURL is not set +# BR2_PACKAGE_LIBDNET is not set +# BR2_PACKAGE_LIBEXOSIP2 is not set +# BR2_PACKAGE_LIBFCGI is not set +# BR2_PACKAGE_LIBGSASL is not set +# BR2_PACKAGE_LIBHTP is not set +# BR2_PACKAGE_LIBHTTPPARSER is not set +# BR2_PACKAGE_LIBHTTPSERVER is not set +# BR2_PACKAGE_LIBIDN is not set +# BR2_PACKAGE_LIBIDN2 is not set +# BR2_PACKAGE_LIBISCSI is not set +# BR2_PACKAGE_LIBKRB5 is not set +# BR2_PACKAGE_LIBLDNS is not set +# BR2_PACKAGE_LIBMAXMINDDB is not set +# BR2_PACKAGE_LIBMBUS is not set +# BR2_PACKAGE_LIBMEMCACHED is not set +# BR2_PACKAGE_LIBMICROHTTPD is not set +# BR2_PACKAGE_LIBMINIUPNPC is not set +# BR2_PACKAGE_LIBMNL is not set +# BR2_PACKAGE_LIBMODBUS is not set +# BR2_PACKAGE_LIBMODSECURITY is not set +# BR2_PACKAGE_LIBNATPMP is not set +# BR2_PACKAGE_LIBNDP is not set +# BR2_PACKAGE_LIBNET is not set +# BR2_PACKAGE_LIBNETCONF2 is not set +# BR2_PACKAGE_LIBNETFILTER_ACCT is not set +# BR2_PACKAGE_LIBNETFILTER_CONNTRACK is not set +# BR2_PACKAGE_LIBNETFILTER_CTHELPER is not set +# BR2_PACKAGE_LIBNETFILTER_CTTIMEOUT is not set +# BR2_PACKAGE_LIBNETFILTER_LOG is not set +# BR2_PACKAGE_LIBNETFILTER_QUEUE is not set +# BR2_PACKAGE_LIBNFNETLINK is not set +# BR2_PACKAGE_LIBNFTNL is not set +# BR2_PACKAGE_LIBNICE is not set +# BR2_PACKAGE_LIBNIDS is not set +# BR2_PACKAGE_LIBNL is not set +# BR2_PACKAGE_LIBNPUPNP is not set +# BR2_PACKAGE_LIBOAUTH is not set +# BR2_PACKAGE_LIBOPING is not set +# BR2_PACKAGE_LIBOSIP2 is not set +# BR2_PACKAGE_LIBPAGEKITE is not set +# BR2_PACKAGE_LIBPCAP is not set +# BR2_PACKAGE_LIBPJSIP is not set +# BR2_PACKAGE_LIBRSYNC is not set +# BR2_PACKAGE_LIBSHAIRPLAY is not set +# BR2_PACKAGE_LIBSHOUT is not set +# BR2_PACKAGE_LIBSOCKETCAN is not set +# BR2_PACKAGE_LIBSOUP is not set +# BR2_PACKAGE_LIBSRTP is not set +# BR2_PACKAGE_LIBSTROPHE is not set +# BR2_PACKAGE_LIBTELNET is not set +# BR2_PACKAGE_LIBTIRPC is not set +# BR2_PACKAGE_LIBTORRENT is not set +# BR2_PACKAGE_LIBTORRENT_RASTERBAR is not set +# BR2_PACKAGE_LIBUHTTPD is not set +# BR2_PACKAGE_LIBUPNP is not set +# BR2_PACKAGE_LIBUPNP18 is not set +# BR2_PACKAGE_LIBUPNPP is not set +# BR2_PACKAGE_LIBURIPARSER is not set +# BR2_PACKAGE_LIBUWSC is not set +# BR2_PACKAGE_LIBVNCSERVER is not set +# BR2_PACKAGE_LIBWEBSOCK is not set +# BR2_PACKAGE_LIBWEBSOCKETS is not set +# BR2_PACKAGE_LIBYANG is not set +# BR2_PACKAGE_LKSCTP_TOOLS is not set +# BR2_PACKAGE_MBUFFER is not set +# BR2_PACKAGE_MONGOOSE is not set +# BR2_PACKAGE_NANOMSG is not set +# BR2_PACKAGE_NEON is not set +# BR2_PACKAGE_NETOPEER2 is not set +# BR2_PACKAGE_NGHTTP2 is not set +# BR2_PACKAGE_NORM is not set +# BR2_PACKAGE_NSS_MYHOSTNAME is not set +# BR2_PACKAGE_NSS_PAM_LDAPD is not set +# BR2_PACKAGE_OMNIORB is not set +# BR2_PACKAGE_OPENLDAP is not set +# BR2_PACKAGE_OPENMPI is not set +# BR2_PACKAGE_OPENPGM is not set +# BR2_PACKAGE_OPENZWAVE is not set +# BR2_PACKAGE_ORTP is not set +# BR2_PACKAGE_PAHO_MQTT_C is not set +# BR2_PACKAGE_PAHO_MQTT_CPP is not set +# BR2_PACKAGE_PISTACHE is not set +# BR2_PACKAGE_QDECODER is not set +# BR2_PACKAGE_QPID_PROTON is not set +# BR2_PACKAGE_RABBITMQ_C is not set +# BR2_PACKAGE_RESIPROCATE is not set +# BR2_PACKAGE_RESTCLIENT_CPP is not set +# BR2_PACKAGE_RTMPDUMP is not set +# BR2_PACKAGE_SLIRP is not set +# BR2_PACKAGE_SNMPPP is not set +# BR2_PACKAGE_SOFIA_SIP is not set +# BR2_PACKAGE_SYSREPO is not set +# BR2_PACKAGE_THRIFT is not set +# BR2_PACKAGE_USBREDIR is not set +# BR2_PACKAGE_WAMPCC is not set +# BR2_PACKAGE_WEBSOCKETPP is not set +# BR2_PACKAGE_ZEROMQ is not set +# BR2_PACKAGE_ZMQPP is not set +# BR2_PACKAGE_ZYRE is not set + +# +# Other +# +# BR2_PACKAGE_APR is not set +# BR2_PACKAGE_APR_UTIL is not set +# BR2_PACKAGE_ARMADILLO is not set +# BR2_PACKAGE_ATF is not set +# BR2_PACKAGE_AVRO_C is not set +# BR2_PACKAGE_BCTOOLBOX is not set +# BR2_PACKAGE_BELR is not set +# BR2_PACKAGE_BOOST is not set +# BR2_PACKAGE_C_CAPNPROTO is not set +# BR2_PACKAGE_CAPNPROTO is not set +# BR2_PACKAGE_CCTZ is not set +# BR2_PACKAGE_CEREAL is not set +# BR2_PACKAGE_CLAPACK is not set +# BR2_PACKAGE_CMOCKA is not set +# BR2_PACKAGE_CPPCMS is not set +# BR2_PACKAGE_CRACKLIB is not set +# BR2_PACKAGE_DAWGDIC is not set +# BR2_PACKAGE_DING_LIBS is not set +# BR2_PACKAGE_EIGEN is not set +# BR2_PACKAGE_ELFUTILS is not set +# BR2_PACKAGE_ELL is not set +# BR2_PACKAGE_FFTW is not set +# BR2_PACKAGE_FLANN is not set +# BR2_PACKAGE_FLATBUFFERS is not set +# BR2_PACKAGE_FLATCC is not set +# BR2_PACKAGE_GCONF is not set +# BR2_PACKAGE_GFLAGS is not set +# BR2_PACKAGE_GLI is not set +# BR2_PACKAGE_GLIBMM is not set +# BR2_PACKAGE_GLM is not set +# BR2_PACKAGE_GMP is not set +BR2_PACKAGE_GOBJECT_INTROSPECTION_ARCH_SUPPORTS=y +# BR2_PACKAGE_GOBJECT_INTROSPECTION is not set +# BR2_PACKAGE_GSL is not set +# BR2_PACKAGE_GTEST is not set +BR2_PACKAGE_JEMALLOC_ARCH_SUPPORTS=y +# BR2_PACKAGE_JEMALLOC is not set + +# +# lapack/blas needs a toolchain w/ fortran +# +BR2_PACKAGE_LIBABSEIL_CPP_ARCH_SUPPORTS=y +# BR2_PACKAGE_LIBABSEIL_CPP is not set +# BR2_PACKAGE_LIBARGTABLE2 is not set +# BR2_PACKAGE_LIBAVL is not set +# BR2_PACKAGE_LIBB64 is not set +BR2_PACKAGE_LIBBSD_ARCH_SUPPORTS=y +# BR2_PACKAGE_LIBBSD is not set +# BR2_PACKAGE_LIBCAP is not set +BR2_PACKAGE_LIBCAP_NG=y +# BR2_PACKAGE_LIBCGROUP is not set +# BR2_PACKAGE_LIBCORRECT is not set +# BR2_PACKAGE_LIBCROCO is not set +# BR2_PACKAGE_LIBCROSSGUID is not set +# BR2_PACKAGE_LIBCSV is not set +# BR2_PACKAGE_LIBDAEMON is not set +# BR2_PACKAGE_LIBEE is not set +# BR2_PACKAGE_LIBEV is not set +# BR2_PACKAGE_LIBEVDEV is not set +# BR2_PACKAGE_LIBEVENT is not set +BR2_PACKAGE_LIBFFI=y +# BR2_PACKAGE_LIBGEE is not set +# BR2_PACKAGE_LIBGLIB2 is not set +# BR2_PACKAGE_LIBGLOB is not set +# BR2_PACKAGE_LIBICAL is not set +# BR2_PACKAGE_LIBITE is not set +# BR2_PACKAGE_LIBLINEAR is not set +# BR2_PACKAGE_LIBLOKI is not set +BR2_PACKAGE_LIBNPTH=y +BR2_PACKAGE_LIBNSPR_ARCH_SUPPORT=y +# BR2_PACKAGE_LIBNSPR is not set +# BR2_PACKAGE_LIBPFM4 is not set +# BR2_PACKAGE_LIBPLIST is not set +# BR2_PACKAGE_LIBPTHREAD_STUBS is not set +BR2_PACKAGE_LIBPTHSEM=y +BR2_PACKAGE_LIBPTHSEM_COMPAT=y +# BR2_PACKAGE_LIBPWQUALITY is not set +# BR2_PACKAGE_LIBSIGC is not set +BR2_PACKAGE_LIBSIGSEGV_ARCH_SUPPORTS=y +# BR2_PACKAGE_LIBSIGSEGV is not set +# BR2_PACKAGE_LIBSPATIALINDEX is not set +# BR2_PACKAGE_LIBTASN1 is not set +# BR2_PACKAGE_LIBTOMMATH is not set +# BR2_PACKAGE_LIBTPL is not set +# BR2_PACKAGE_LIBUBOX is not set +# BR2_PACKAGE_LIBUCI is not set +BR2_PACKAGE_LIBURCU_ARCH_SUPPORTS=y +# BR2_PACKAGE_LIBURCU is not set +# BR2_PACKAGE_LIBUV is not set +# BR2_PACKAGE_LIGHTNING is not set +# BR2_PACKAGE_LINUX_PAM is not set +# BR2_PACKAGE_LIQUID_DSP is not set +# BR2_PACKAGE_LTTNG_LIBUST is not set +# BR2_PACKAGE_MATIO is not set +# BR2_PACKAGE_MPC is not set +# BR2_PACKAGE_MPDECIMAL is not set +# BR2_PACKAGE_MPFR is not set +# BR2_PACKAGE_MPIR is not set +# BR2_PACKAGE_MSGPACK is not set +# BR2_PACKAGE_ORC is not set +# BR2_PACKAGE_P11_KIT is not set +BR2_PACKAGE_POCO_ARCH_SUPPORTS=y +# BR2_PACKAGE_POCO is not set +BR2_PACKAGE_PROTOBUF_ARCH_SUPPORTS=y +# BR2_PACKAGE_PROTOBUF is not set +# BR2_PACKAGE_PROTOBUF_C is not set +# BR2_PACKAGE_QHULL is not set +# BR2_PACKAGE_QLIBC is not set +# BR2_PACKAGE_RIEMANN_C_CLIENT is not set +# BR2_PACKAGE_SHAPELIB is not set +# BR2_PACKAGE_SKALIBS is not set +# BR2_PACKAGE_SPHINXBASE is not set +# BR2_PACKAGE_TINYCBOR is not set +# BR2_PACKAGE_UVW is not set +# BR2_PACKAGE_XAPIAN is not set + +# +# Security +# +# BR2_PACKAGE_LIBAPPARMOR is not set +# BR2_PACKAGE_LIBSELINUX is not set +# BR2_PACKAGE_LIBSEPOL is not set +# BR2_PACKAGE_SAFECLIB is not set + +# +# Text and terminal handling +# +# BR2_PACKAGE_AUGEAS is not set +# BR2_PACKAGE_ENCHANT is not set +# BR2_PACKAGE_FMT is not set +# BR2_PACKAGE_FSTRCMP is not set +# BR2_PACKAGE_ICU is not set +# BR2_PACKAGE_LIBCLI is not set +# BR2_PACKAGE_LIBEDIT is not set +# BR2_PACKAGE_LIBENCA is not set +# BR2_PACKAGE_LIBESTR is not set +# BR2_PACKAGE_LIBFRIBIDI is not set +# BR2_PACKAGE_LIBUNISTRING is not set +# BR2_PACKAGE_LINENOISE is not set +BR2_PACKAGE_NCURSES=y +# BR2_PACKAGE_NCURSES_WCHAR is not set +# BR2_PACKAGE_NCURSES_TARGET_PROGS is not set +BR2_PACKAGE_NCURSES_ADDITIONAL_TERMINFO="" +# BR2_PACKAGE_NEWT is not set +# BR2_PACKAGE_ONIGURUMA is not set +# BR2_PACKAGE_PCRE is not set +# BR2_PACKAGE_PCRE2 is not set +# BR2_PACKAGE_POPT is not set +BR2_PACKAGE_READLINE=y +# BR2_PACKAGE_SLANG is not set +# BR2_PACKAGE_TCLAP is not set +# BR2_PACKAGE_UTF8PROC is not set + +# +# Mail +# +# BR2_PACKAGE_DOVECOT is not set +# BR2_PACKAGE_EXIM is not set +# BR2_PACKAGE_FETCHMAIL is not set +# BR2_PACKAGE_HEIRLOOM_MAILX is not set +# BR2_PACKAGE_LIBESMTP is not set +# BR2_PACKAGE_MSMTP is not set +# BR2_PACKAGE_MUTT is not set + +# +# Miscellaneous +# +# BR2_PACKAGE_AESPIPE is not set +# BR2_PACKAGE_BC is not set +BR2_PACKAGE_BITCOIN_ARCH_SUPPORTS=y +# BR2_PACKAGE_BITCOIN is not set +# BR2_PACKAGE_CLAMAV is not set +# BR2_PACKAGE_COLLECTD is not set +# BR2_PACKAGE_COLLECTL is not set +# BR2_PACKAGE_DOMOTICZ is not set +# BR2_PACKAGE_EMPTY is not set +# BR2_PACKAGE_GNURADIO is not set +# BR2_PACKAGE_GOOGLEFONTDIRECTORY is not set + +# +# gqrx needs a toolchain w/ C++, threads, wchar, dynamic library +# + +# +# gqrx needs qt5 +# +# BR2_PACKAGE_GSETTINGS_DESKTOP_SCHEMAS is not set +# BR2_PACKAGE_HAVEGED is not set +# BR2_PACKAGE_LINUX_SYSCALL_SUPPORT is not set +# BR2_PACKAGE_MCRYPT is not set +# BR2_PACKAGE_MOBILE_BROADBAND_PROVIDER_INFO is not set +# BR2_PACKAGE_NETDATA is not set +# BR2_PACKAGE_PROJ is not set +# BR2_PACKAGE_QPDF is not set +# BR2_PACKAGE_SHARED_MIME_INFO is not set +# BR2_PACKAGE_SUNWAIT is not set +# BR2_PACKAGE_TASKD is not set +# BR2_PACKAGE_XUTIL_UTIL_MACROS is not set + +# +# Networking applications +# +# BR2_PACKAGE_AIRCRACK_NG is not set +# BR2_PACKAGE_AOETOOLS is not set +# BR2_PACKAGE_APACHE is not set +# BR2_PACKAGE_ARGUS is not set +# BR2_PACKAGE_ARP_SCAN is not set +# BR2_PACKAGE_ARPTABLES is not set +# BR2_PACKAGE_ASTERISK is not set +# BR2_PACKAGE_ATFTP is not set +# BR2_PACKAGE_AVAHI is not set +# BR2_PACKAGE_AXEL is not set +# BR2_PACKAGE_BABELD is not set +# BR2_PACKAGE_BANDWIDTHD is not set +# BR2_PACKAGE_BATCTL is not set +# BR2_PACKAGE_BCUSDK is not set +# BR2_PACKAGE_BIND is not set +# BR2_PACKAGE_BIRD is not set +# BR2_PACKAGE_BLUEZ5_UTILS is not set +# BR2_PACKAGE_BMON is not set +# BR2_PACKAGE_BOA is not set +# BR2_PACKAGE_BOINC is not set +# BR2_PACKAGE_BRCM_PATCHRAM_PLUS is not set +# BR2_PACKAGE_BRIDGE_UTILS is not set +# BR2_PACKAGE_BWM_NG is not set +# BR2_PACKAGE_C_ICAP is not set +# BR2_PACKAGE_CAN_UTILS is not set +# BR2_PACKAGE_CANNELLONI is not set +# BR2_PACKAGE_CHRONY is not set +# BR2_PACKAGE_CIVETWEB is not set +# BR2_PACKAGE_CONNMAN is not set + +# +# connman-gtk needs libgtk3 and a glibc or uClibc toolchain w/ wchar, threads, resolver, dynamic library +# +# BR2_PACKAGE_CONNTRACK_TOOLS is not set +# BR2_PACKAGE_CORKSCREW is not set +# BR2_PACKAGE_CRDA is not set +# BR2_PACKAGE_CTORRENT is not set +# BR2_PACKAGE_CUPS is not set +# BR2_PACKAGE_DANTE is not set +# BR2_PACKAGE_DARKHTTPD is not set +# BR2_PACKAGE_DEHYDRATED is not set +# BR2_PACKAGE_DHCPCD is not set +# BR2_PACKAGE_DHCPDUMP is not set +# BR2_PACKAGE_DNSMASQ is not set +# BR2_PACKAGE_DRBD_UTILS is not set +# BR2_PACKAGE_DROPBEAR is not set +# BR2_PACKAGE_EBTABLES is not set + +# +# ejabberd needs erlang, toolchain w/ C++ +# +# BR2_PACKAGE_ETHTOOL is not set +# BR2_PACKAGE_FAIFA is not set +# BR2_PACKAGE_FAIL2BAN is not set +# BR2_PACKAGE_FASTD is not set +# BR2_PACKAGE_FCGIWRAP is not set +# BR2_PACKAGE_FPING is not set +# BR2_PACKAGE_FREESWITCH is not set +# BR2_PACKAGE_GERBERA is not set +# BR2_PACKAGE_GESFTPSERVER is not set +# BR2_PACKAGE_GLOOX is not set +# BR2_PACKAGE_GLORYTUN is not set + +# +# gupnp-tools needs libgtk3 +# +# BR2_PACKAGE_HANS is not set +BR2_PACKAGE_HAPROXY_ARCH_SUPPORTS=y +# BR2_PACKAGE_HAPROXY is not set +# BR2_PACKAGE_HIAWATHA is not set +# BR2_PACKAGE_HOSTAPD is not set +# BR2_PACKAGE_HTTPING is not set +# BR2_PACKAGE_I2PD is not set +# BR2_PACKAGE_IBRDTN_TOOLS is not set +# BR2_PACKAGE_IBRDTND is not set +# BR2_PACKAGE_IFMETRIC is not set +# BR2_PACKAGE_IFTOP is not set +BR2_PACKAGE_IFUPDOWN_SCRIPTS=y +# BR2_PACKAGE_IGD2_FOR_LINUX is not set + +# +# igh-ethercat needs a Linux kernel to be built +# +# BR2_PACKAGE_IGMPPROXY is not set +# BR2_PACKAGE_INADYN is not set +# BR2_PACKAGE_IODINE is not set +# BR2_PACKAGE_IPERF is not set +# BR2_PACKAGE_IPERF3 is not set +# BR2_PACKAGE_IPROUTE2 is not set +# BR2_PACKAGE_IPSEC_TOOLS is not set +# BR2_PACKAGE_IPSET is not set +# BR2_PACKAGE_IPTABLES is not set +# BR2_PACKAGE_IPTRAF_NG is not set +# BR2_PACKAGE_IPUTILS is not set +# BR2_PACKAGE_IRSSI is not set +# BR2_PACKAGE_IW is not set +# BR2_PACKAGE_IWD is not set +# BR2_PACKAGE_JANUS_GATEWAY is not set +# BR2_PACKAGE_KEEPALIVED is not set +# BR2_PACKAGE_KISMET is not set +# BR2_PACKAGE_KNOCK is not set +# BR2_PACKAGE_LEAFNODE2 is not set +# BR2_PACKAGE_LFT is not set +# BR2_PACKAGE_LFTP is not set +# BR2_PACKAGE_LIGHTTPD is not set +# BR2_PACKAGE_LINKNX is not set +# BR2_PACKAGE_LINKS is not set +# BR2_PACKAGE_LINPHONE is not set +# BR2_PACKAGE_LINUX_ZIGBEE is not set +# BR2_PACKAGE_LINUXPTP is not set +# BR2_PACKAGE_LLDPD is not set +# BR2_PACKAGE_LRZSZ is not set +# BR2_PACKAGE_LYNX is not set +# BR2_PACKAGE_MACCHANGER is not set +# BR2_PACKAGE_MEMCACHED is not set +# BR2_PACKAGE_MII_DIAG is not set +# BR2_PACKAGE_MINI_SNMPD is not set +# BR2_PACKAGE_MINIDLNA is not set +# BR2_PACKAGE_MINISSDPD is not set +# BR2_PACKAGE_MJPG_STREAMER is not set +# BR2_PACKAGE_MODEM_MANAGER is not set +BR2_PACKAGE_MONGREL2_LIBC_SUPPORTS=y +# BR2_PACKAGE_MONGREL2 is not set +# BR2_PACKAGE_MONKEY is not set +# BR2_PACKAGE_MOSH is not set +# BR2_PACKAGE_MOSQUITTO is not set +# BR2_PACKAGE_MROUTED is not set +# BR2_PACKAGE_MTR is not set +# BR2_PACKAGE_NBD is not set +# BR2_PACKAGE_NCFTP is not set +# BR2_PACKAGE_NDISC6 is not set +# BR2_PACKAGE_NETATALK is not set +# BR2_PACKAGE_NETPLUG is not set +# BR2_PACKAGE_NETSNMP is not set +# BR2_PACKAGE_NETSTAT_NAT is not set + +# +# NetworkManager needs udev /dev management and a glibc toolchain w/ headers >= 3.2, dynamic library, wchar, threads +# +# BR2_PACKAGE_NFACCT is not set +# BR2_PACKAGE_NFTABLES is not set +# BR2_PACKAGE_NGINX is not set +# BR2_PACKAGE_NGIRCD is not set +# BR2_PACKAGE_NGREP is not set +# BR2_PACKAGE_NLOAD is not set +# BR2_PACKAGE_NMAP is not set +# BR2_PACKAGE_NOIP is not set +# BR2_PACKAGE_NTP is not set +# BR2_PACKAGE_NUTTCP is not set +# BR2_PACKAGE_ODHCP6C is not set +# BR2_PACKAGE_ODHCPLOC is not set +# BR2_PACKAGE_OLSR is not set +# BR2_PACKAGE_OPEN_LLDP is not set +# BR2_PACKAGE_OPEN_PLC_UTILS is not set +# BR2_PACKAGE_OPENNTPD is not set +# BR2_PACKAGE_OPENOBEX is not set +# BR2_PACKAGE_OPENRESOLV is not set +# BR2_PACKAGE_OPENSSH is not set +# BR2_PACKAGE_OPENSWAN is not set +# BR2_PACKAGE_OPENVPN is not set +# BR2_PACKAGE_P910ND is not set +# BR2_PACKAGE_PARPROUTED is not set +# BR2_PACKAGE_PHIDGETWEBSERVICE is not set +# BR2_PACKAGE_PHYTOOL is not set +# BR2_PACKAGE_PIMD is not set +# BR2_PACKAGE_PIXIEWPS is not set +# BR2_PACKAGE_POUND is not set +# BR2_PACKAGE_PPPD is not set +# BR2_PACKAGE_PPTP_LINUX is not set +# BR2_PACKAGE_PRIVOXY is not set +# BR2_PACKAGE_PROFTPD is not set +# BR2_PACKAGE_PROSODY is not set +# BR2_PACKAGE_PROXYCHAINS_NG is not set +# BR2_PACKAGE_PTPD is not set +# BR2_PACKAGE_PTPD2 is not set +# BR2_PACKAGE_PURE_FTPD is not set +# BR2_PACKAGE_PUTTY is not set +# BR2_PACKAGE_QUAGGA is not set + +# +# rabbitmq-server needs erlang +# +# BR2_PACKAGE_RADVD is not set +# BR2_PACKAGE_REAVER is not set +# BR2_PACKAGE_RP_PPPOE is not set +# BR2_PACKAGE_RPCBIND is not set +# BR2_PACKAGE_RSH_REDONE is not set +# BR2_PACKAGE_RSYNC is not set +# BR2_PACKAGE_RTORRENT is not set +# BR2_PACKAGE_RTPTOOLS is not set +# BR2_PACKAGE_RYGEL is not set +# BR2_PACKAGE_S6_DNS is not set +# BR2_PACKAGE_S6_NETWORKING is not set +# BR2_PACKAGE_SAMBA4 is not set +# BR2_PACKAGE_SCONESERVER is not set +# BR2_PACKAGE_SER2NET is not set +# BR2_PACKAGE_SHADOWSOCKS_LIBEV is not set +# BR2_PACKAGE_SHAIRPORT_SYNC is not set +# BR2_PACKAGE_SHELLINABOX is not set +# BR2_PACKAGE_SMCROUTE is not set +# BR2_PACKAGE_SNGREP is not set +# BR2_PACKAGE_SNMPCLITOOLS is not set +# BR2_PACKAGE_SNORT is not set +# BR2_PACKAGE_SOCAT is not set +# BR2_PACKAGE_SOCKETCAND is not set +# BR2_PACKAGE_SOFTETHER is not set +# BR2_PACKAGE_SPAWN_FCGI is not set +# BR2_PACKAGE_SPICE_PROTOCOL is not set +# BR2_PACKAGE_SQUID is not set +# BR2_PACKAGE_SSHGUARD is not set +# BR2_PACKAGE_SSHPASS is not set +# BR2_PACKAGE_SSLH is not set +# BR2_PACKAGE_STRONGSWAN is not set +# BR2_PACKAGE_STUNNEL is not set +# BR2_PACKAGE_SURICATA is not set +# BR2_PACKAGE_TCPDUMP is not set +# BR2_PACKAGE_TCPING is not set +# BR2_PACKAGE_TCPREPLAY is not set +# BR2_PACKAGE_THTTPD is not set +# BR2_PACKAGE_TINC is not set +# BR2_PACKAGE_TINYHTTPD is not set +# BR2_PACKAGE_TINYPROXY is not set +# BR2_PACKAGE_TINYSSH is not set +# BR2_PACKAGE_TOR is not set +# BR2_PACKAGE_TRACEROUTE is not set +# BR2_PACKAGE_TRANSMISSION is not set +# BR2_PACKAGE_TUNCTL is not set +# BR2_PACKAGE_TVHEADEND is not set +# BR2_PACKAGE_UACME is not set +# BR2_PACKAGE_UDPCAST is not set +# BR2_PACKAGE_UFTP is not set +# BR2_PACKAGE_UHTTPD is not set +# BR2_PACKAGE_ULOGD is not set +# BR2_PACKAGE_UNBOUND is not set +# BR2_PACKAGE_USHARE is not set +# BR2_PACKAGE_USSP_PUSH is not set +# BR2_PACKAGE_VDE2 is not set +# BR2_PACKAGE_VDR is not set +# BR2_PACKAGE_VNSTAT is not set +# BR2_PACKAGE_VPNC is not set +# BR2_PACKAGE_VSFTPD is not set +# BR2_PACKAGE_VTUN is not set +# BR2_PACKAGE_WAVEMON is not set +# BR2_PACKAGE_WIREGUARD_TOOLS is not set +# BR2_PACKAGE_WIRELESS_REGDB is not set +# BR2_PACKAGE_WIRELESS_TOOLS is not set +# BR2_PACKAGE_WIRESHARK is not set +# BR2_PACKAGE_WPA_SUPPLICANT is not set +# BR2_PACKAGE_WPAN_TOOLS is not set +# BR2_PACKAGE_XINETD is not set +# BR2_PACKAGE_XL2TP is not set + +# +# xtables-addons needs a Linux kernel to be built +# +# BR2_PACKAGE_ZNC is not set + +# +# Package managers +# + +# +# ------------------------------------------------------- +# + +# +# Please note: +# + +# +# - Buildroot does *not* generate binary packages, +# + +# +# - Buildroot does *not* install any package database. +# + +# +# * +# + +# +# It is up to you to provide those by yourself if you +# + +# +# want to use any of those package managers. +# + +# +# * +# + +# +# See the manual: +# + +# +# http://buildroot.org/manual.html#faq-no-binary-packages +# + +# +# ------------------------------------------------------- +# +# BR2_PACKAGE_OPKG is not set + +# +# Real-Time +# +# BR2_PACKAGE_XENOMAI is not set + +# +# Security +# +# BR2_PACKAGE_APPARMOR is not set +# BR2_PACKAGE_CHECKPOLICY is not set +# BR2_PACKAGE_IMA_EVM_UTILS is not set +# BR2_PACKAGE_OPTEE_BENCHMARK is not set +# BR2_PACKAGE_OPTEE_CLIENT is not set +# BR2_PACKAGE_PAXTEST is not set +# BR2_PACKAGE_REFPOLICY is not set +# BR2_PACKAGE_RESTORECOND is not set +# BR2_PACKAGE_SELINUX_PYTHON is not set +# BR2_PACKAGE_SEMODULE_UTILS is not set +# BR2_PACKAGE_SETOOLS is not set +# BR2_PACKAGE_URANDOM_SCRIPTS is not set + +# +# Shell and utilities +# + +# +# Shells +# +# BR2_PACKAGE_MKSH is not set +# BR2_PACKAGE_ZSH is not set + +# +# Utilities +# +# BR2_PACKAGE_AT is not set +BR2_PACKAGE_CCRYPT=y +# BR2_PACKAGE_CRUDINI is not set +# BR2_PACKAGE_DIALOG is not set +# BR2_PACKAGE_DTACH is not set +# BR2_PACKAGE_EASY_RSA is not set +# BR2_PACKAGE_FILE is not set +BR2_PACKAGE_GNUPG2=y +# BR2_PACKAGE_GNUPG2_GPGV is not set +# BR2_PACKAGE_INOTIFY_TOOLS is not set +# BR2_PACKAGE_LOCKFILE_PROGS is not set +# BR2_PACKAGE_LOGROTATE is not set +# BR2_PACKAGE_LOGSURFER is not set +# BR2_PACKAGE_PDMENU is not set +# BR2_PACKAGE_PINENTRY is not set +# BR2_PACKAGE_RANGER is not set +# BR2_PACKAGE_RTTY is not set +# BR2_PACKAGE_SCREEN is not set +# BR2_PACKAGE_SUDO is not set +# BR2_PACKAGE_TINI is not set +# BR2_PACKAGE_TMUX is not set +# BR2_PACKAGE_TTYD is not set +# BR2_PACKAGE_XMLSTARLET is not set +# BR2_PACKAGE_XXHASH is not set +# BR2_PACKAGE_YTREE is not set + +# +# System tools +# +BR2_PACKAGE_ACL=y +# BR2_PACKAGE_ANDROID_TOOLS is not set +# BR2_PACKAGE_ATOP is not set +BR2_PACKAGE_ATTR=y +# BR2_PACKAGE_BUBBLEWRAP is not set +# BR2_PACKAGE_CGROUPFS_MOUNT is not set +# BR2_PACKAGE_CIRCUS is not set +# BR2_PACKAGE_CPULOAD is not set +# BR2_PACKAGE_DAEMON is not set +# BR2_PACKAGE_DC3DD is not set +# BR2_PACKAGE_DDRESCUE is not set +# BR2_PACKAGE_DOCKER_COMPOSE is not set +# BR2_PACKAGE_EARLYOOM is not set + +# +# emlog needs a Linux kernel to be built +# +# BR2_PACKAGE_FTOP is not set +# BR2_PACKAGE_GETENT is not set +# BR2_PACKAGE_HTOP is not set +# BR2_PACKAGE_IBM_SW_TPM2 is not set +# BR2_PACKAGE_IOTOP is not set +# BR2_PACKAGE_IPRUTILS is not set +# BR2_PACKAGE_IRQBALANCE is not set +# BR2_PACKAGE_KEYUTILS is not set +# BR2_PACKAGE_KMOD is not set +# BR2_PACKAGE_LIBOSTREE is not set +# BR2_PACKAGE_LXC is not set +# BR2_PACKAGE_MFOC is not set +# BR2_PACKAGE_MONIT is not set +# BR2_PACKAGE_NCDU is not set + +# +# netifrc needs openrc as init system +# +# BR2_PACKAGE_NUT is not set + +# +# pamtester depends on linux-pam +# +# BR2_PACKAGE_POLKIT is not set +# BR2_PACKAGE_PROCRANK_LINUX is not set +# BR2_PACKAGE_PWGEN is not set +# BR2_PACKAGE_QUOTA is not set +# BR2_PACKAGE_QUOTATOOL is not set +# BR2_PACKAGE_RAUC is not set +BR2_PACKAGE_RNDADDENTROPY=y +# BR2_PACKAGE_S6 is not set +# BR2_PACKAGE_S6_LINUX_INIT is not set +# BR2_PACKAGE_S6_LINUX_UTILS is not set +# BR2_PACKAGE_S6_PORTABLE_UTILS is not set +# BR2_PACKAGE_S6_RC is not set +# BR2_PACKAGE_SCRUB is not set +# BR2_PACKAGE_SCRYPT is not set + +# +# sdbusplus needs systemd and a toolchain w/ C++, gcc >= 7 +# +# BR2_PACKAGE_SMACK is not set +# BR2_PACKAGE_SUPERVISOR is not set +# BR2_PACKAGE_SWUPDATE is not set +BR2_PACKAGE_SYSTEMD_ARCH_SUPPORTS=y +# BR2_PACKAGE_TPM_TOOLS is not set +# BR2_PACKAGE_TPM2_ABRMD is not set +# BR2_PACKAGE_TPM2_TOOLS is not set +# BR2_PACKAGE_TPM2_TOTP is not set +# BR2_PACKAGE_UNSCD is not set +BR2_PACKAGE_UTIL_LINUX=y +BR2_PACKAGE_UTIL_LINUX_LIBBLKID=y +BR2_PACKAGE_UTIL_LINUX_LIBFDISK=y +BR2_PACKAGE_UTIL_LINUX_LIBMOUNT=y +BR2_PACKAGE_UTIL_LINUX_LIBSMARTCOLS=y +BR2_PACKAGE_UTIL_LINUX_LIBUUID=y +BR2_PACKAGE_UTIL_LINUX_BINARIES=y +BR2_PACKAGE_UTIL_LINUX_AGETTY=y +BR2_PACKAGE_UTIL_LINUX_BFS=y +# BR2_PACKAGE_UTIL_LINUX_CAL is not set +# BR2_PACKAGE_UTIL_LINUX_CHFN_CHSH is not set +# BR2_PACKAGE_UTIL_LINUX_CHMEM is not set +BR2_PACKAGE_UTIL_LINUX_CRAMFS=y +# BR2_PACKAGE_UTIL_LINUX_EJECT is not set +BR2_PACKAGE_UTIL_LINUX_FALLOCATE=y +BR2_PACKAGE_UTIL_LINUX_FDFORMAT=y +BR2_PACKAGE_UTIL_LINUX_FSCK=y +# BR2_PACKAGE_UTIL_LINUX_HARDLINK is not set +BR2_PACKAGE_UTIL_LINUX_HWCLOCK=y +# BR2_PACKAGE_UTIL_LINUX_IPCRM is not set +# BR2_PACKAGE_UTIL_LINUX_IPCS is not set +BR2_PACKAGE_UTIL_LINUX_KILL=y +BR2_PACKAGE_UTIL_LINUX_LAST=y +BR2_PACKAGE_UTIL_LINUX_LINE=y +# BR2_PACKAGE_UTIL_LINUX_LOGGER is not set +# BR2_PACKAGE_UTIL_LINUX_LOGIN is not set +BR2_PACKAGE_UTIL_LINUX_LOSETUP=y +# BR2_PACKAGE_UTIL_LINUX_LSLOGINS is not set +# BR2_PACKAGE_UTIL_LINUX_LSMEM is not set +BR2_PACKAGE_UTIL_LINUX_MESG=y +BR2_PACKAGE_UTIL_LINUX_MINIX=y +BR2_PACKAGE_UTIL_LINUX_MORE=y +BR2_PACKAGE_UTIL_LINUX_MOUNT=y +BR2_PACKAGE_UTIL_LINUX_MOUNTPOINT=y +BR2_PACKAGE_UTIL_LINUX_NEWGRP=y +BR2_PACKAGE_UTIL_LINUX_NOLOGIN=y +BR2_PACKAGE_UTIL_LINUX_NSENTER=y +# BR2_PACKAGE_UTIL_LINUX_PG is not set +BR2_PACKAGE_UTIL_LINUX_PARTX=y +BR2_PACKAGE_UTIL_LINUX_PIVOT_ROOT=y +BR2_PACKAGE_UTIL_LINUX_RAW=y +BR2_PACKAGE_UTIL_LINUX_RENAME=y +# BR2_PACKAGE_UTIL_LINUX_RFKILL is not set +# BR2_PACKAGE_UTIL_LINUX_RUNUSER is not set +BR2_PACKAGE_UTIL_LINUX_SCHEDUTILS=y +BR2_PACKAGE_UTIL_LINUX_SETPRIV=y +BR2_PACKAGE_UTIL_LINUX_SETTERM=y +# BR2_PACKAGE_UTIL_LINUX_SU is not set +# BR2_PACKAGE_UTIL_LINUX_SULOGIN is not set +BR2_PACKAGE_UTIL_LINUX_SWITCH_ROOT=y +BR2_PACKAGE_UTIL_LINUX_TUNELP=y +BR2_PACKAGE_UTIL_LINUX_UL=y +BR2_PACKAGE_UTIL_LINUX_UNSHARE=y +BR2_PACKAGE_UTIL_LINUX_UTMPDUMP=y +BR2_PACKAGE_UTIL_LINUX_UUIDD=y +BR2_PACKAGE_UTIL_LINUX_VIPW=y +BR2_PACKAGE_UTIL_LINUX_WALL=y +# BR2_PACKAGE_UTIL_LINUX_WIPEFS is not set +BR2_PACKAGE_UTIL_LINUX_WDCTL=y +BR2_PACKAGE_UTIL_LINUX_WRITE=y +BR2_PACKAGE_UTIL_LINUX_ZRAMCTL=y +# BR2_PACKAGE_XDG_DBUS_PROXY is not set +BR2_PACKAGE_MACHINE_EMULATOR_TOOLS=y +BR2_PACKAGE_MACHINE_EMULATOR_TOOLS_YIELD=y +BR2_PACKAGE_MACHINE_EMULATOR_TOOLS_DEHASH=y +BR2_PACKAGE_MACHINE_EMULATOR_TOOLS_FLASHDRIVE=y +BR2_PACKAGE_MACHINE_EMULATOR_TOOLS_READBE64=y +BR2_PACKAGE_MACHINE_EMULATOR_TOOLS_WRITEBE64=y + +# +# Text editors and viewers +# +# BR2_PACKAGE_ED is not set +# BR2_PACKAGE_JOE is not set +# BR2_PACKAGE_MC is not set +# BR2_PACKAGE_MOST is not set +# BR2_PACKAGE_NANO is not set +BR2_PACKAGE_UEMACS=y + +# +# Filesystem images +# +# BR2_TARGET_ROOTFS_AXFS is not set +# BR2_TARGET_ROOTFS_BTRFS is not set +# BR2_TARGET_ROOTFS_CLOOP is not set +# BR2_TARGET_ROOTFS_CPIO is not set +# BR2_TARGET_ROOTFS_CRAMFS is not set +# BR2_TARGET_ROOTFS_EROFS is not set +BR2_TARGET_ROOTFS_EXT2=y +BR2_TARGET_ROOTFS_EXT2_2=y +# BR2_TARGET_ROOTFS_EXT2_2r0 is not set +BR2_TARGET_ROOTFS_EXT2_2r1=y +# BR2_TARGET_ROOTFS_EXT2_3 is not set +# BR2_TARGET_ROOTFS_EXT2_4 is not set +BR2_TARGET_ROOTFS_EXT2_GEN=2 +BR2_TARGET_ROOTFS_EXT2_REV=1 +BR2_TARGET_ROOTFS_EXT2_LABEL="root" +BR2_TARGET_ROOTFS_EXT2_SIZE="60M" +BR2_TARGET_ROOTFS_EXT2_INODES=0 +BR2_TARGET_ROOTFS_EXT2_RESBLKS=0 +BR2_TARGET_ROOTFS_EXT2_MKFS_OPTIONS="-O ^64bit" +BR2_TARGET_ROOTFS_EXT2_NONE=y +# BR2_TARGET_ROOTFS_EXT2_GZIP is not set +# BR2_TARGET_ROOTFS_EXT2_BZIP2 is not set +# BR2_TARGET_ROOTFS_EXT2_LZ4 is not set +# BR2_TARGET_ROOTFS_EXT2_LZMA is not set +# BR2_TARGET_ROOTFS_EXT2_LZO is not set +# BR2_TARGET_ROOTFS_EXT2_XZ is not set +# BR2_TARGET_ROOTFS_F2FS is not set + +# +# initramfs needs a Linux kernel to be built +# +# BR2_TARGET_ROOTFS_JFFS2 is not set +# BR2_TARGET_ROOTFS_ROMFS is not set +# BR2_TARGET_ROOTFS_SQUASHFS is not set +# BR2_TARGET_ROOTFS_TAR is not set +# BR2_TARGET_ROOTFS_UBI is not set +# BR2_TARGET_ROOTFS_UBIFS is not set +# BR2_TARGET_ROOTFS_YAFFS2 is not set + +# +# Bootloaders +# +# BR2_TARGET_BAREBOX is not set +# BR2_TARGET_OPENSBI is not set +# BR2_TARGET_UBOOT is not set + +# +# Host utilities +# +# BR2_PACKAGE_HOST_AESPIPE is not set +# BR2_PACKAGE_HOST_ANDROID_TOOLS is not set +# BR2_PACKAGE_HOST_BTRFS_PROGS is not set +# BR2_PACKAGE_HOST_CHECKPOLICY is not set +# BR2_PACKAGE_HOST_CHECKSEC is not set +# BR2_PACKAGE_HOST_CMAKE is not set +# BR2_PACKAGE_HOST_CRAMFS is not set +# BR2_PACKAGE_HOST_CRYPTSETUP is not set +# BR2_PACKAGE_HOST_DBUS_PYTHON is not set +# BR2_PACKAGE_HOST_DFU_UTIL is not set +# BR2_PACKAGE_HOST_DOS2UNIX is not set +# BR2_PACKAGE_HOST_DOSFSTOOLS is not set +# BR2_PACKAGE_HOST_DOXYGEN is not set +# BR2_PACKAGE_HOST_DTC is not set +BR2_PACKAGE_HOST_E2FSPROGS=y +# BR2_PACKAGE_HOST_E2TOOLS is not set +# BR2_PACKAGE_HOST_EROFS_UTILS is not set +# BR2_PACKAGE_HOST_EXFATPROGS is not set +# BR2_PACKAGE_HOST_F2FS_TOOLS is not set +# BR2_PACKAGE_HOST_FAKETIME is not set +# BR2_PACKAGE_HOST_FATCAT is not set +# BR2_PACKAGE_HOST_FWUP is not set +BR2_PACKAGE_HOST_GENEXT2FS=y +# BR2_PACKAGE_HOST_GENIMAGE is not set +# BR2_PACKAGE_HOST_GENPART is not set +# BR2_PACKAGE_HOST_GNUPG is not set +BR2_PACKAGE_HOST_GO_HOST_ARCH_SUPPORTS=y +BR2_PACKAGE_HOST_GO_BOOTSTRAP_ARCH_SUPPORTS=y +BR2_PACKAGE_HOST_GOOGLE_BREAKPAD_ARCH_SUPPORTS=y +# BR2_PACKAGE_HOST_GPTFDISK is not set +# BR2_PACKAGE_HOST_IMAGEMAGICK is not set +# BR2_PACKAGE_HOST_IMX_MKIMAGE is not set +# BR2_PACKAGE_HOST_JQ is not set +# BR2_PACKAGE_HOST_JSMIN is not set +# BR2_PACKAGE_HOST_LIBP11 is not set +# BR2_PACKAGE_HOST_LPC3250LOADER is not set +# BR2_PACKAGE_HOST_LTTNG_BABELTRACE is not set +# BR2_PACKAGE_HOST_MENDER_ARTIFACT is not set +# BR2_PACKAGE_HOST_MKPASSWD is not set +# BR2_PACKAGE_HOST_MTD is not set +# BR2_PACKAGE_HOST_MTOOLS is not set +# BR2_PACKAGE_HOST_ODB is not set +# BR2_PACKAGE_HOST_OPENOCD is not set +# BR2_PACKAGE_HOST_OPKG_UTILS is not set +# BR2_PACKAGE_HOST_PARTED is not set +BR2_PACKAGE_HOST_PATCHELF=y +# BR2_PACKAGE_HOST_PIGZ is not set +# BR2_PACKAGE_HOST_PKGCONF is not set +# BR2_PACKAGE_HOST_PWGEN is not set +# BR2_PACKAGE_HOST_PYTHON is not set +# BR2_PACKAGE_HOST_PYTHON_CYTHON is not set +# BR2_PACKAGE_HOST_PYTHON_LXML is not set +# BR2_PACKAGE_HOST_PYTHON_SIX is not set +# BR2_PACKAGE_HOST_PYTHON_XLRD is not set +# BR2_PACKAGE_HOST_PYTHON3 is not set +BR2_PACKAGE_HOST_QEMU_ARCH_SUPPORTS=y +BR2_PACKAGE_HOST_QEMU_SYSTEM_ARCH_SUPPORTS=y +BR2_PACKAGE_HOST_QEMU_USER_ARCH_SUPPORTS=y +# BR2_PACKAGE_HOST_QEMU is not set +# BR2_PACKAGE_HOST_RAUC is not set +# BR2_PACKAGE_HOST_RCW is not set +BR2_PACKAGE_HOST_RUSTC_ARCH_SUPPORTS=y +BR2_PACKAGE_HOST_RUSTC_ARCH="riscv64" +# BR2_PACKAGE_HOST_RUSTC is not set +BR2_PACKAGE_PROVIDES_HOST_RUSTC="host-rust-bin" +# BR2_PACKAGE_HOST_SAM_BA is not set +# BR2_PACKAGE_HOST_SDBUSPLUS is not set +# BR2_PACKAGE_HOST_SQUASHFS is not set +# BR2_PACKAGE_HOST_SWIG is not set +# BR2_PACKAGE_HOST_UBOOT_TOOLS is not set +BR2_PACKAGE_HOST_UTIL_LINUX=y +# BR2_PACKAGE_HOST_UTP_COM is not set +# BR2_PACKAGE_HOST_VBOOT_UTILS is not set +# BR2_PACKAGE_HOST_XORRISO is not set +# BR2_PACKAGE_HOST_ZIP is not set +# BR2_PACKAGE_HOST_ZSTD is not set + +# +# Legacy config options +# + +# +# Legacy options removed in 2020.08 +# +# BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_AMD64 is not set +# BR2_KERNEL_HEADERS_5_6 is not set +# BR2_KERNEL_HEADERS_5_5 is not set +# BR2_BINUTILS_VERSION_2_31_X is not set +# BR2_PACKAGE_KODI_PERIPHERAL_STEAMCONTROLLER is not set + +# +# Legacy options removed in 2020.05 +# +# BR2_PACKAGE_WIRINGPI is not set +# BR2_PACKAGE_PYTHON_PYCRYPTO is not set +# BR2_PACKAGE_MTDEV2TUIO is not set +# BR2_PACKAGE_EZXML is not set +# BR2_PACKAGE_COLLECTD_LVM is not set +# BR2_PACKAGE_PYTHON_PYASN is not set +# BR2_PACKAGE_PYTHON_PYASN_MODULES is not set +# BR2_PACKAGE_LINUX_FIRMWARE_ATHEROS_10K_QCA6174 is not set +# BR2_PACKAGE_QT5CANVAS3D is not set +# BR2_PACKAGE_KODI_LIBTHEORA is not set +# BR2_PACKAGE_CEGUI06 is not set +# BR2_GCC_VERSION_5_X is not set + +# +# Legacy options removed in 2020.02 +# +# BR2_PACKAGE_JAMVM is not set +# BR2_PACKAGE_CLASSPATH is not set +# BR2_PACKAGE_QT5_VERSION_5_6 is not set +# BR2_PACKAGE_CURL is not set +# BR2_PACKAGE_GSTREAMER is not set +# BR2_PACKAGE_NVIDIA_TEGRA23_BINARIES_GSTREAMER_PLUGINS is not set +# BR2_PACKAGE_NVIDIA_TEGRA23_BINARIES_NV_SAMPLE_APPS is not set +# BR2_PACKAGE_FREERDP_GSTREAMER is not set +# BR2_PACKAGE_OPENCV3_WITH_GSTREAMER is not set +# BR2_PACKAGE_OPENCV_WITH_GSTREAMER is not set +# BR2_PACKAGE_LIBPLAYER is not set +# BR2_GCC_VERSION_OR1K is not set +# BR2_PACKAGE_BLUEZ_UTILS is not set +# BR2_PACKAGE_GADGETFS_TEST is not set +# BR2_PACKAGE_FIS is not set +BR2_PACKAGE_REFPOLICY_POLICY_VERSION="" +# BR2_PACKAGE_CELT051 is not set +# BR2_PACKAGE_WIREGUARD is not set +# BR2_PACKAGE_PERL_NET_PING is not set +# BR2_PACKAGE_PERL_MIME_BASE64 is not set +# BR2_PACKAGE_PERL_DIGEST_MD5 is not set +# BR2_PACKAGE_ERLANG_P1_ICONV is not set +# BR2_KERNEL_HEADERS_5_3 is not set +# BR2_PACKAGE_PYTHON_SCAPY3K is not set +# BR2_BINUTILS_VERSION_2_30_X is not set +# BR2_PACKAGE_RPI_USERLAND_START_VCFILED is not set + +# +# Legacy options removed in 2019.11 +# +# BR2_PACKAGE_OPENVMTOOLS_PROCPS is not set +# BR2_PACKAGE_ALLJOYN is not set +# BR2_PACKAGE_ALLJOYN_BASE is not set +# BR2_PACKAGE_ALLJOYN_BASE_CONTROLPANEL is not set +# BR2_PACKAGE_ALLJOYN_BASE_NOTIFICATION is not set +# BR2_PACKAGE_ALLJOYN_BASE_ONBOARDING is not set +# BR2_PACKAGE_ALLJOYN_TCL_BASE is not set +# BR2_PACKAGE_ALLJOYN_TCL is not set +BR2_TOOLCHAIN_EXTRA_EXTERNAL_LIBS="" +# BR2_PACKAGE_PYTHON_PYSNMP_APPS is not set +# BR2_KERNEL_HEADERS_5_2 is not set +# BR2_TARGET_RISCV_PK is not set +# BR2_PACKAGE_SQLITE_STAT3 is not set +# BR2_KERNEL_HEADERS_5_1 is not set +# BR2_PACKAGE_DEVMEM2 is not set +# BR2_PACKAGE_USTR is not set +# BR2_PACKAGE_KODI_SCREENSAVER_PLANESTATE is not set +# BR2_PACKAGE_KODI_VISUALISATION_WAVEFORHUE is not set +# BR2_PACKAGE_KODI_AUDIODECODER_OPUS is not set +# BR2_PACKAGE_MESA3D_OSMESA is not set +# BR2_PACKAGE_HOSTAPD_DRIVER_RTW is not set +# BR2_PACKAGE_WPA_SUPPLICANT_DBUS_NEW is not set +# BR2_PACKAGE_WPA_SUPPLICANT_DBUS_OLD is not set + +# +# Legacy options removed in 2019.08 +# +# BR2_TARGET_TS4800_MBRBOOT is not set +# BR2_PACKAGE_LIBAMCODEC is not set +# BR2_PACKAGE_ODROID_SCRIPTS is not set +# BR2_PACKAGE_ODROID_MALI is not set +# BR2_PACKAGE_KODI_PLATFORM_AML is not set +# BR2_GCC_VERSION_6_X is not set +# BR2_GCC_VERSION_4_9_X is not set +# BR2_GDB_VERSION_7_12 is not set +# BR2_PACKAGE_XAPP_MKFONTDIR is not set +# BR2_GDB_VERSION_8_0 is not set +# BR2_KERNEL_HEADERS_4_20 is not set +# BR2_KERNEL_HEADERS_5_0 is not set + +# +# Legacy options removed in 2019.05 +# +# BR2_CSKY_DSP is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_COMPOSITOR is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_IQA is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_OPENCV is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_STEREO is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_VCD is not set +# BR2_PACKAGE_LUNIT is not set +# BR2_PACKAGE_FFMPEG_FFSERVER is not set +# BR2_PACKAGE_LIBUMP is not set +# BR2_PACKAGE_SUNXI_MALI is not set +# BR2_BINUTILS_VERSION_2_29_X is not set +# BR2_BINUTILS_VERSION_2_28_X is not set +# BR2_PACKAGE_GST_PLUGINS_BAD_PLUGIN_APEXSINK is not set + +# +# Legacy options removed in 2019.02 +# +# BR2_PACKAGE_QT is not set +# BR2_PACKAGE_QTUIO is not set +# BR2_PACKAGE_PINENTRY_QT4 is not set +# BR2_PACKAGE_POPPLER_QT is not set +# BR2_PACKAGE_OPENCV3_WITH_QT is not set +# BR2_PACKAGE_OPENCV_WITH_QT is not set +# BR2_PACKAGE_AMD_CATALYST_CCCLE is not set +# BR2_PACKAGE_SDL_QTOPIA is not set +# BR2_PACKAGE_PYTHON_PYQT is not set +# BR2_PACKAGE_LUACRYPTO is not set +# BR2_PACKAGE_TN5250 is not set +# BR2_PACKAGE_BOOST_SIGNALS is not set +# BR2_PACKAGE_FFTW_PRECISION_SINGLE is not set +# BR2_PACKAGE_FFTW_PRECISION_DOUBLE is not set +# BR2_PACKAGE_FFTW_PRECISION_LONG_DOUBLE is not set +# BR2_PACKAGE_LUA_5_2 is not set +# BR2_TARGET_GENERIC_PASSWD_MD5 is not set + +# +# Legacy options removed in 2018.11 +# +# BR2_TARGET_XLOADER is not set +# BR2_PACKAGE_TIDSP_BINARIES is not set +# BR2_PACKAGE_DSP_TOOLS is not set +# BR2_PACKAGE_GST_DSP is not set +# BR2_PACKAGE_BOOTUTILS is not set +# BR2_PACKAGE_EXPEDITE is not set +# BR2_PACKAGE_MESA3D_OPENGL_TEXTURE_FLOAT is not set +# BR2_KERNEL_HEADERS_4_10 is not set +# BR2_KERNEL_HEADERS_4_11 is not set +# BR2_KERNEL_HEADERS_4_12 is not set +# BR2_KERNEL_HEADERS_4_13 is not set +# BR2_KERNEL_HEADERS_4_15 is not set +# BR2_KERNEL_HEADERS_4_17 is not set +# BR2_PACKAGE_LIBNFTNL_XML is not set +# BR2_KERNEL_HEADERS_3_2 is not set +# BR2_KERNEL_HEADERS_4_1 is not set +# BR2_KERNEL_HEADERS_4_16 is not set +# BR2_KERNEL_HEADERS_4_18 is not set + +# +# Legacy options removed in 2018.08 +# +# BR2_PACKAGE_DOCKER_ENGINE_STATIC_CLIENT is not set +# BR2_PACKAGE_XSERVER_XORG_SERVER_V_1_19 is not set +# BR2_PACKAGE_XPROTO_APPLEWMPROTO is not set +# BR2_PACKAGE_XPROTO_BIGREQSPROTO is not set +# BR2_PACKAGE_XPROTO_COMPOSITEPROTO is not set +# BR2_PACKAGE_XPROTO_DAMAGEPROTO is not set +# BR2_PACKAGE_XPROTO_DMXPROTO is not set +# BR2_PACKAGE_XPROTO_DRI2PROTO is not set +# BR2_PACKAGE_XPROTO_DRI3PROTO is not set +# BR2_PACKAGE_XPROTO_FIXESPROTO is not set +# BR2_PACKAGE_XPROTO_FONTCACHEPROTO is not set +# BR2_PACKAGE_XPROTO_FONTSPROTO is not set +# BR2_PACKAGE_XPROTO_GLPROTO is not set +# BR2_PACKAGE_XPROTO_INPUTPROTO is not set +# BR2_PACKAGE_XPROTO_KBPROTO is not set +# BR2_PACKAGE_XPROTO_PRESENTPROTO is not set +# BR2_PACKAGE_XPROTO_RANDRPROTO is not set +# BR2_PACKAGE_XPROTO_RECORDPROTO is not set +# BR2_PACKAGE_XPROTO_RENDERPROTO is not set +# BR2_PACKAGE_XPROTO_RESOURCEPROTO is not set +# BR2_PACKAGE_XPROTO_SCRNSAVERPROTO is not set +# BR2_PACKAGE_XPROTO_VIDEOPROTO is not set +# BR2_PACKAGE_XPROTO_WINDOWSWMPROTO is not set +# BR2_PACKAGE_XPROTO_XCMISCPROTO is not set +# BR2_PACKAGE_XPROTO_XEXTPROTO is not set +# BR2_PACKAGE_XPROTO_XF86BIGFONTPROTO is not set +# BR2_PACKAGE_XPROTO_XF86DGAPROTO is not set +# BR2_PACKAGE_XPROTO_XF86DRIPROTO is not set +# BR2_PACKAGE_XPROTO_XF86VIDMODEPROTO is not set +# BR2_PACKAGE_XPROTO_XINERAMAPROTO is not set +# BR2_PACKAGE_XPROTO_XPROTO is not set +# BR2_PACKAGE_XPROTO_XPROXYMANAGEMENTPROTOCOL is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_LIB_OPENGL_OPENGL is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_LIB_OPENGL_GLES2 is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_LIB_OPENGL_GLX is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_LIB_OPENGL_EGL is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_LIB_OPENGL_X11 is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_LIB_OPENGL_WAYLAND is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_LIB_OPENGL_DISPMANX is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_AUDIOMIXER is not set +# BR2_PACKAGE_GST1_PLUGINS_UGLY_PLUGIN_LAME is not set +# BR2_PACKAGE_GST1_PLUGINS_UGLY_PLUGIN_MPG123 is not set +# BR2_GDB_VERSION_7_11 is not set +# BR2_GDB_VERSION_7_10 is not set + +# +# Legacy options removed in 2018.05 +# +# BR2_PACKAGE_MEDIAART_BACKEND_NONE is not set +# BR2_PACKAGE_MEDIAART_BACKEND_GDK_PIXBUF is not set +# BR2_PACKAGE_TI_SGX_AM335X is not set +# BR2_PACKAGE_TI_SGX_AM437X is not set +# BR2_PACKAGE_TI_SGX_AM4430 is not set +# BR2_PACKAGE_TI_SGX_AM5430 is not set +# BR2_PACKAGE_JANUS_AUDIO_BRIDGE is not set +# BR2_PACKAGE_JANUS_ECHO_TEST is not set +# BR2_PACKAGE_JANUS_RECORDPLAY is not set +# BR2_PACKAGE_JANUS_SIP_GATEWAY is not set +# BR2_PACKAGE_JANUS_STREAMING is not set +# BR2_PACKAGE_JANUS_TEXT_ROOM is not set +# BR2_PACKAGE_JANUS_VIDEO_CALL is not set +# BR2_PACKAGE_JANUS_VIDEO_ROOM is not set +# BR2_PACKAGE_JANUS_MQTT is not set +# BR2_PACKAGE_JANUS_RABBITMQ is not set +# BR2_PACKAGE_JANUS_REST is not set +# BR2_PACKAGE_JANUS_UNIX_SOCKETS is not set +# BR2_PACKAGE_JANUS_WEBSOCKETS is not set +# BR2_PACKAGE_IPSEC_SECCTX_DISABLE is not set +# BR2_PACKAGE_IPSEC_SECCTX_ENABLE is not set +# BR2_PACKAGE_IPSEC_SECCTX_KERNEL is not set +# BR2_PACKAGE_LIBTFDI_CPP is not set +# BR2_PACKAGE_JQUERY_UI_THEME_BLACK_TIE is not set +# BR2_PACKAGE_JQUERY_UI_THEME_BLITZER is not set +# BR2_PACKAGE_JQUERY_UI_THEME_CUPERTINO is not set +# BR2_PACKAGE_JQUERY_UI_THEME_DARK_HIVE is not set +# BR2_PACKAGE_JQUERY_UI_THEME_DOT_LUV is not set +# BR2_PACKAGE_JQUERY_UI_THEME_EGGPLANT is not set +# BR2_PACKAGE_JQUERY_UI_THEME_EXCITE_BIKE is not set +# BR2_PACKAGE_JQUERY_UI_THEME_FLICK is not set +# BR2_PACKAGE_JQUERY_UI_THEME_HOT_SNEAKS is not set +# BR2_PACKAGE_JQUERY_UI_THEME_HUMANITY is not set +# BR2_PACKAGE_JQUERY_UI_THEME_LE_FROG is not set +# BR2_PACKAGE_JQUERY_UI_THEME_MINT_CHOC is not set +# BR2_PACKAGE_JQUERY_UI_THEME_OVERCAST is not set +# BR2_PACKAGE_JQUERY_UI_THEME_PEPPER_GRINDER is not set +# BR2_PACKAGE_JQUERY_UI_THEME_REDMOND is not set +# BR2_PACKAGE_JQUERY_UI_THEME_SMOOTHNESS is not set +# BR2_PACKAGE_JQUERY_UI_THEME_SOUTH_STREET is not set +# BR2_PACKAGE_JQUERY_UI_THEME_START is not set +# BR2_PACKAGE_JQUERY_UI_THEME_SUNNY is not set +# BR2_PACKAGE_JQUERY_UI_THEME_SWANKY_PURSE is not set +# BR2_PACKAGE_JQUERY_UI_THEME_TRONTASTIC is not set +# BR2_PACKAGE_JQUERY_UI_THEME_UI_DARKNESS is not set +# BR2_PACKAGE_JQUERY_UI_THEME_UI_LIGHTNESS is not set +# BR2_PACKAGE_JQUERY_UI_THEME_VADER is not set +# BR2_PACKAGE_BLUEZ5_PLUGINS_HEALTH is not set +# BR2_PACKAGE_BLUEZ5_PLUGINS_MIDI is not set +# BR2_PACKAGE_BLUEZ5_PLUGINS_NFC is not set +# BR2_PACKAGE_BLUEZ5_PLUGINS_SAP is not set +# BR2_PACKAGE_BLUEZ5_PLUGINS_SIXAXIS is not set +# BR2_PACKAGE_TRANSMISSION_REMOTE is not set +# BR2_PACKAGE_LIBKCAPI_APPS is not set +# BR2_PACKAGE_MPLAYER is not set +# BR2_PACKAGE_MPLAYER_MPLAYER is not set +# BR2_PACKAGE_MPLAYER_MENCODER is not set +# BR2_PACKAGE_LIBPLAYER_MPLAYER is not set +# BR2_PACKAGE_IQVLINUX is not set +# BR2_BINFMT_FLAT_SEP_DATA is not set +# BR2_bfin is not set +# BR2_PACKAGE_KODI_ADSP_BASIC is not set +# BR2_PACKAGE_KODI_ADSP_FREESURROUND is not set + +# +# Legacy options removed in 2018.02 +# +# BR2_KERNEL_HEADERS_3_4 is not set +# BR2_KERNEL_HEADERS_3_10 is not set +# BR2_KERNEL_HEADERS_3_12 is not set +# BR2_BINUTILS_VERSION_2_27_X is not set +# BR2_PACKAGE_EEPROG is not set +# BR2_PACKAGE_GNUPG2_GPGV2 is not set +# BR2_PACKAGE_IMX_GPU_VIV_APITRACE is not set +# BR2_PACKAGE_IMX_GPU_VIV_G2D is not set + +# +# Legacy options removed in 2017.11 +# +# BR2_PACKAGE_RFKILL is not set +# BR2_PACKAGE_UTIL_LINUX_RESET is not set +# BR2_PACKAGE_POLICYCOREUTILS_AUDIT2ALLOW is not set +# BR2_PACKAGE_POLICYCOREUTILS_RESTORECOND is not set +# BR2_PACKAGE_SEPOLGEN is not set +# BR2_PACKAGE_OPENOBEX_BLUEZ is not set +# BR2_PACKAGE_OPENOBEX_LIBUSB is not set +# BR2_PACKAGE_OPENOBEX_APPS is not set +# BR2_PACKAGE_OPENOBEX_SYSLOG is not set +# BR2_PACKAGE_OPENOBEX_DUMP is not set +# BR2_PACKAGE_AICCU is not set +# BR2_PACKAGE_UTIL_LINUX_LOGIN_UTILS is not set + +# +# Legacy options removed in 2017.08 +# +# BR2_TARGET_GRUB is not set +# BR2_PACKAGE_SIMICSFS is not set +# BR2_BINUTILS_VERSION_2_26_X is not set +BR2_XTENSA_OVERLAY_DIR="" +BR2_XTENSA_CUSTOM_NAME="" +# BR2_PACKAGE_HOST_MKE2IMG is not set +BR2_TARGET_ROOTFS_EXT2_BLOCKS=0 +BR2_TARGET_ROOTFS_EXT2_EXTRA_INODES=0 +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_CDXAPARSE is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DATAURISRC is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DCCP is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_HDVPARSE is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_MVE is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_NUVDEMUX is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_PATCHDETECT is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_SDI is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_TTA is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_VIDEOMEASURE is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_APEXSINK is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_SDL is not set +# BR2_PACKAGE_GST1_PLUGINS_UGLY_PLUGIN_MAD is not set +# BR2_STRIP_none is not set +# BR2_PACKAGE_BEECRYPT_CPP is not set +# BR2_PACKAGE_SPICE_CLIENT is not set +# BR2_PACKAGE_SPICE_GUI is not set +# BR2_PACKAGE_SPICE_TUNNEL is not set +# BR2_PACKAGE_INPUT_TOOLS is not set +# BR2_PACKAGE_INPUT_TOOLS_INPUTATTACH is not set +# BR2_PACKAGE_INPUT_TOOLS_JSCAL is not set +# BR2_PACKAGE_INPUT_TOOLS_JSTEST is not set +# BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_SH is not set +# BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_X86 is not set +# BR2_GCC_VERSION_4_8_X is not set + +# +# Legacy options removed in 2017.05 +# +# BR2_PACKAGE_SUNXI_MALI_R2P4 is not set +# BR2_PACKAGE_NODEJS_MODULES_COFFEESCRIPT is not set +# BR2_PACKAGE_NODEJS_MODULES_EXPRESS is not set +# BR2_PACKAGE_BLUEZ5_UTILS_GATTTOOL is not set +# BR2_PACKAGE_OPENOCD_FT2XXX is not set +# BR2_PACKAGE_KODI_RTMPDUMP is not set +# BR2_PACKAGE_KODI_VISUALISATION_FOUNTAIN is not set +# BR2_PACKAGE_PORTMAP is not set +# BR2_BINUTILS_VERSION_2_25_X is not set +# BR2_TOOLCHAIN_BUILDROOT_INET_RPC is not set +BR2_TARGET_ROOTFS_EXT2_EXTRA_BLOCKS=0 +# BR2_PACKAGE_SYSTEMD_KDBUS is not set +# BR2_PACKAGE_POLARSSL is not set +# BR2_NBD_CLIENT is not set +# BR2_NBD_SERVER is not set +# BR2_PACKAGE_GMOCK is not set +# BR2_KERNEL_HEADERS_4_8 is not set +# BR2_KERNEL_HEADERS_3_18 is not set +# BR2_GLIBC_VERSION_2_22 is not set + +# +# Legacy options removed in 2017.02 +# +# BR2_PACKAGE_PERL_DB_FILE is not set +# BR2_KERNEL_HEADERS_4_7 is not set +# BR2_KERNEL_HEADERS_4_6 is not set +# BR2_KERNEL_HEADERS_4_5 is not set +# BR2_KERNEL_HEADERS_3_14 is not set +# BR2_TOOLCHAIN_EXTERNAL_MUSL_CROSS is not set +# BR2_UCLIBC_INSTALL_TEST_SUITE is not set +# BR2_TOOLCHAIN_EXTERNAL_BLACKFIN_UCLINUX is not set +# BR2_PACKAGE_MAKEDEVS is not set +# BR2_TOOLCHAIN_EXTERNAL_ARAGO_ARMV7A is not set +# BR2_TOOLCHAIN_EXTERNAL_ARAGO_ARMV5TE is not set +# BR2_PACKAGE_SNOWBALL_HDMISERVICE is not set +# BR2_PACKAGE_SNOWBALL_INIT is not set +# BR2_GDB_VERSION_7_9 is not set + +# +# Legacy options removed in 2016.11 +# +# BR2_PACKAGE_PHP_SAPI_CLI_CGI is not set +# BR2_PACKAGE_PHP_SAPI_CLI_FPM is not set +# BR2_PACKAGE_WVSTREAMS is not set +# BR2_PACKAGE_WVDIAL is not set +# BR2_PACKAGE_WEBKITGTK24 is not set +# BR2_PACKAGE_TORSMO is not set +# BR2_PACKAGE_SSTRIP is not set +# BR2_KERNEL_HEADERS_4_3 is not set +# BR2_KERNEL_HEADERS_4_2 is not set +# BR2_PACKAGE_KODI_ADDON_XVDR is not set +# BR2_PACKAGE_IPKG is not set +# BR2_GCC_VERSION_4_7_X is not set +# BR2_BINUTILS_VERSION_2_24_X is not set +# BR2_PACKAGE_WESTON_RPI is not set +# BR2_GCC_VERSION_4_8_ARC is not set +# BR2_KERNEL_HEADERS_4_0 is not set +# BR2_KERNEL_HEADERS_3_19 is not set +# BR2_PACKAGE_LIBEVAS_GENERIC_LOADERS is not set +# BR2_PACKAGE_ELEMENTARY is not set +# BR2_LINUX_KERNEL_CUSTOM_LOCAL is not set + +# +# Legacy options removed in 2016.08 +# +# BR2_PACKAGE_EFL_JP2K is not set +# BR2_PACKAGE_SYSTEMD_COMPAT is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_LIVEADDER is not set +# BR2_PACKAGE_LIBFSLVPUWRAP is not set +# BR2_PACKAGE_LIBFSLPARSER is not set +# BR2_PACKAGE_LIBFSLCODEC is not set +# BR2_PACKAGE_UBOOT_TOOLS_MKIMAGE_FIT_SIGNATURE_SUPPORT is not set +# BR2_PTHREADS_OLD is not set +# BR2_BINUTILS_VERSION_2_23_X is not set +# BR2_TOOLCHAIN_BUILDROOT_EGLIBC is not set +# BR2_GDB_VERSION_7_8 is not set + +# +# Legacy options removed in 2016.05 +# +# BR2_PACKAGE_OPENVPN_CRYPTO_POLARSSL is not set +# BR2_PACKAGE_NGINX_HTTP_SPDY_MODULE is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_RTP is not set +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_MPG123 is not set +# BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_POWERPC is not set +# BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_POWERPC_E500V2 is not set +# BR2_x86_i386 is not set +# BR2_PACKAGE_QT5QUICK1 is not set +BR2_TARGET_UBOOT_CUSTOM_PATCH_DIR="" +# BR2_PACKAGE_XDRIVER_XF86_INPUT_VOID is not set +# BR2_KERNEL_HEADERS_3_17 is not set +# BR2_GDB_VERSION_7_7 is not set +# BR2_PACKAGE_FOOMATIC_FILTERS is not set +# BR2_PACKAGE_SAMBA is not set +# BR2_PACKAGE_KODI_WAVPACK is not set +# BR2_PACKAGE_KODI_RSXS is not set +# BR2_PACKAGE_KODI_GOOM is not set +# BR2_PACKAGE_SYSTEMD_ALL_EXTRAS is not set +# BR2_GCC_VERSION_4_5_X is not set +# BR2_PACKAGE_SQLITE_READLINE is not set + +# +# Legacy options removed in 2016.02 +# +# BR2_PACKAGE_DOVECOT_BZIP2 is not set +# BR2_PACKAGE_DOVECOT_ZLIB is not set +# BR2_PACKAGE_E2FSPROGS_FINDFS is not set +# BR2_PACKAGE_OPENPOWERLINK_DEBUG_LEVEL is not set +# BR2_PACKAGE_OPENPOWERLINK_KERNEL_MODULE is not set +# BR2_PACKAGE_OPENPOWERLINK_LIBPCAP is not set +# BR2_LINUX_KERNEL_SAME_AS_HEADERS is not set +# BR2_PACKAGE_CUPS_PDFTOPS is not set +# BR2_KERNEL_HEADERS_3_16 is not set +# BR2_PACKAGE_PYTHON_PYXML is not set +# BR2_ENABLE_SSP is not set +# BR2_PACKAGE_DIRECTFB_CLE266 is not set +# BR2_PACKAGE_DIRECTFB_UNICHROME is not set +# BR2_PACKAGE_LIBELEMENTARY is not set +# BR2_PACKAGE_LIBEINA is not set +# BR2_PACKAGE_LIBEET is not set +# BR2_PACKAGE_LIBEVAS is not set +# BR2_PACKAGE_LIBECORE is not set +# BR2_PACKAGE_LIBEDBUS is not set +# BR2_PACKAGE_LIBEFREET is not set +# BR2_PACKAGE_LIBEIO is not set +# BR2_PACKAGE_LIBEMBRYO is not set +# BR2_PACKAGE_LIBEDJE is not set +# BR2_PACKAGE_LIBETHUMB is not set +# BR2_PACKAGE_INFOZIP is not set +# BR2_BR2_PACKAGE_NODEJS_0_10_X is not set +# BR2_BR2_PACKAGE_NODEJS_0_12_X is not set +# BR2_BR2_PACKAGE_NODEJS_4_X is not set + +# +# Legacy options removed in 2015.11 +# +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_REAL is not set +# BR2_PACKAGE_MEDIA_CTL is not set +# BR2_PACKAGE_SCHIFRA is not set +# BR2_PACKAGE_ZXING is not set +# BR2_PACKAGE_BLACKBOX is not set +# BR2_KERNEL_HEADERS_3_0 is not set +# BR2_KERNEL_HEADERS_3_11 is not set +# BR2_KERNEL_HEADERS_3_13 is not set +# BR2_KERNEL_HEADERS_3_15 is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_ANDI is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_BLTLOAD is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_CPULOAD is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_DATABUFFER is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_DIOLOAD is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_DOK is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_DRIVERTEST is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_FIRE is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_FLIP is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_FONTS is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_INPUT is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_JOYSTICK is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_KNUCKLES is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_LAYER is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_MATRIX is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_MATRIX_WATER is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_NEO is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_NETLOAD is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_PALETTE is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_PARTICLE is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_PORTER is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_STRESS is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_TEXTURE is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_VIDEO is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_VIDEO_PARTICLE is not set +# BR2_PACKAGE_DIRECTFB_EXAMPLES_WINDOW is not set +# BR2_PACKAGE_KOBS_NG is not set +# BR2_PACKAGE_SAWMAN is not set +# BR2_PACKAGE_DIVINE is not set + +# +# Legacy options removed in 2015.08 +# +# BR2_PACKAGE_KODI_PVR_ADDONS is not set +# BR2_BINUTILS_VERSION_2_23_2 is not set +# BR2_BINUTILS_VERSION_2_24 is not set +# BR2_BINUTILS_VERSION_2_25 is not set +# BR2_PACKAGE_PERF is not set +# BR2_BINUTILS_VERSION_2_22 is not set +# BR2_PACKAGE_GPU_VIV_BIN_MX6Q is not set +# BR2_TARGET_UBOOT_NETWORK is not set diff --git a/cartesi-python-chess-cartesi-img/cartesi-python-chess.sh b/cartesi-python-chess-cartesi-img/cartesi-python-chess.sh new file mode 100755 index 0000000..6ec37b2 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/cartesi-python-chess.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -x + + +/mnt/cartesi-python-chess/python-chess-validate.py + +exit + diff --git a/cartesi-python-chess-cartesi-img/chess-game-broken.pgn b/cartesi-python-chess-cartesi-img/chess-game-broken.pgn new file mode 100644 index 0000000..edb6f56 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess-game-broken.pgn @@ -0,0 +1,47 @@ +[Event "Grand Slam Final Masters"] +[Site "Bilbao ESP"] +[Date "2010.10.11"] +[Round "3"] +[White "Shirov, Alexei"] +[Black "Carlsen, Magnus"] +[Result "1/2-1/2"] +[WhiteTitle "GM"] +[BlackTitle "GM"] +[WhiteElo "2749"] +[BlackElo "2826"] +[ECO "C95"] +[Opening "Ruy Lopez"] +[Variation "closed, Breyer, Borisenko variation"] +[WhiteFideId "2209390"] +[BlackFideId "1503014"] +[EventDate "2010.10.09"] +[EventType "DRR"] + +1. e4 e5 2. Nf3 Nc8 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 +O-O 9. h3 Nb8 10. d4 Nbd7 11. Nbd2 Bb7 12. Bc2 Re8 13. a4 Bf8 14. Bd3 c6 15. b4 +Nb6 16. axb5 cxb5 17. d5 Rc8 18. Bb2 Nh5 19. Bf1 f5 20. Nxe5 Rxe5 21. c4 Nf6 22. +Bxe5 dxe5 23. c5 fxe4 24. d6 Na4 25. Nc4 Nxc5 26. bxc5 Rxc5 27. Qb3 Bd5 28. Qb4 +Rxc4 29. Bxc4 bxc4 30. Rxa6 Qc8 31. Qa5 c3 32. d7 Qxd7 33. Qxc3 Qb7 34. Rea1 Qb8 +35. Ra7 h6 36. Rc7 Qb6 37. Qxe5 Bd6 38. Rc8+ Kf7 39. Qc3 Qb7 40. Rca8 Qxa8 41. +Rxa8 Bxa8 42. Qd4 Bd5 43. Qa7+ Be7 44. Qc7 h5 45. Qe5 g6 46. Kh2 Be6 47. Kg1 Bf5 +48. Kf1 Ne8 49. Kg1 Ng7 50. Qd5+ Ne6 51. Qb7 h4 52. Kh2 Nf4 53. Kg1 Nd3 54. Kf1 +Nc5 55. Qd5+ Be6 56. Qe5 Bf5 57. Qd5+ Ne6 58. Qb7 Kf8 59. Qa8+ Bd8 60. Qa7 Bf6 +61. Qb7 Kg8 62. Qa7 Bg5 63. Ke2 Nf4+ 64. Kf1 Nh5 65. Ke2 Kf8 66. Kf1 Bf6 67. Qc7 +Ng7 68. Qa7 Ne6 69. Qb7 Nd8 70. Qa7 Nf7 71. Kg1 Kg7 72. Kf1 Be5 73. Qe7 g5 74. +Kg1 Bf6 75. Qb7 Kg6 76. Qd5 Ne5 77. Qg8+ Bg7 78. Qe8+ Kh6 79. Qe7 Nd3 80. Kf1 +Nf4 81. Qd6+ Bg6 82. Qe7 Nd5 83. Qe6 Nf6 84. Kg1 Be8 85. Qf5 Bd7 86. Qe5 Kg6 87. +Kf1 Bf8 88. Kg1 Ba3 89. Qc7 Bb4 90. Qe5 Bd2 91. Qd4 Bf4 92. Qb6 Be8 93. Kf1 Bf7 +94. Kg1 Kf5 95. Qa7 Be6 96. Kf1 Kg6 97. Qb6 Bd7 98. Kg1 Ba4 99. Qe6 Bb5 100. Qb6 +Bc4 101. Qd4 Be6 102. Kf1 Bf5 103. Kg1 g4 104. hxg4 Bxg4 105. Qc3 Bf5 106. Qb3 +Bg5 107. Kf1 Kh6 108. Kg1 Kg6 109. Kf1 Bd7 110. Kg1 Be8 111. Qc3 Bf7 112. Qe5 +Bd5 113. Qc3 Be6 114. Qe5 Bd7 115. Qc3 Bf5 116. Qb3 Nh5 117. Qg8+ Ng7 118. Qb3 +Ne6 119. Kh2 Bf6 120. Kg1 Bg7 121. Kf1 Ng5 122. Qb6+ Bf6 123. Kg1 h3 124. gxh3 +Bxh3 125. Qd6 Bf5 126. Kg2 Nf3 127. Qd5 Kg5 128. Qg8+ Bg6 129. Qd5+ Be5 130. +Qd8+ Kh5 131. Qd5 Bf5 132. Qf7+ Kg4 133. Qg8+ Ng5 134. Qc4 Bf4 135. Qg8 Be6 136. +Qg7 Bf7 137. Qd4 Kf5 138. Qc5+ Be5 139. Qf8 Kg6 140. Qc5 Bf6 141. Qd6 Bc4 142. +Qc6 Be6 143. Qd6 Bg4 144. Qd5 Bf3+ 145. Kf1 Nf7 146. Kg1 Ne5 147. Qg8+ Kf5 148. +Qc8+ Kg5 149. Qg8+ Ng6 150. Qd5+ Kh6 151. Qe6 Be5 152. Qf5 Bf4 153. Qf6 Bg5 154. +Qe6 Kg7 155. Qd7+ Ne7 156. Qe6 Bf6 157. Kf1 Kg6 158. Kg1 Nf5 159. Qg8+ Kh5 160. +Qf7+ Kg5 161. Qg8+ Kf4 162. Qb8+ Kg4 163. Qg8+ Bg5 164. Qc8 Bf6 165. Qg8+ Kf4 +166. Qb8+ Be5 167. Qb4 Nd4 168. Qf8+ Kg5 169. Qg8+ Kh6 170. Qf8+ Bg7 171. Qd6+ +Kh5 172. Qh2+ Kg5 173. Qg3+ Bg4 174. Qe3+ Kf5 1/2-1/2 diff --git a/cartesi-python-chess-cartesi-img/chess-game-ok.pgn b/cartesi-python-chess-cartesi-img/chess-game-ok.pgn new file mode 100644 index 0000000..acb8206 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess-game-ok.pgn @@ -0,0 +1,47 @@ +[Event "Grand Slam Final Masters"] +[Site "Bilbao ESP"] +[Date "2010.10.11"] +[Round "3"] +[White "Shirov, Alexei"] +[Black "Carlsen, Magnus"] +[Result "1/2-1/2"] +[WhiteTitle "GM"] +[BlackTitle "GM"] +[WhiteElo "2749"] +[BlackElo "2826"] +[ECO "C95"] +[Opening "Ruy Lopez"] +[Variation "closed, Breyer, Borisenko variation"] +[WhiteFideId "2209390"] +[BlackFideId "1503014"] +[EventDate "2010.10.09"] +[EventType "DRR"] + +1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 +O-O 9. h3 Nb8 10. d4 Nbd7 11. Nbd2 Bb7 12. Bc2 Re8 13. a4 Bf8 14. Bd3 c6 15. b4 +Nb6 16. axb5 cxb5 17. d5 Rc8 18. Bb2 Nh5 19. Bf1 f5 20. Nxe5 Rxe5 21. c4 Nf6 22. +Bxe5 dxe5 23. c5 fxe4 24. d6 Na4 25. Nc4 Nxc5 26. bxc5 Rxc5 27. Qb3 Bd5 28. Qb4 +Rxc4 29. Bxc4 bxc4 30. Rxa6 Qc8 31. Qa5 c3 32. d7 Qxd7 33. Qxc3 Qb7 34. Rea1 Qb8 +35. Ra7 h6 36. Rc7 Qb6 37. Qxe5 Bd6 38. Rc8+ Kf7 39. Qc3 Qb7 40. Rca8 Qxa8 41. +Rxa8 Bxa8 42. Qd4 Bd5 43. Qa7+ Be7 44. Qc7 h5 45. Qe5 g6 46. Kh2 Be6 47. Kg1 Bf5 +48. Kf1 Ne8 49. Kg1 Ng7 50. Qd5+ Ne6 51. Qb7 h4 52. Kh2 Nf4 53. Kg1 Nd3 54. Kf1 +Nc5 55. Qd5+ Be6 56. Qe5 Bf5 57. Qd5+ Ne6 58. Qb7 Kf8 59. Qa8+ Bd8 60. Qa7 Bf6 +61. Qb7 Kg8 62. Qa7 Bg5 63. Ke2 Nf4+ 64. Kf1 Nh5 65. Ke2 Kf8 66. Kf1 Bf6 67. Qc7 +Ng7 68. Qa7 Ne6 69. Qb7 Nd8 70. Qa7 Nf7 71. Kg1 Kg7 72. Kf1 Be5 73. Qe7 g5 74. +Kg1 Bf6 75. Qb7 Kg6 76. Qd5 Ne5 77. Qg8+ Bg7 78. Qe8+ Kh6 79. Qe7 Nd3 80. Kf1 +Nf4 81. Qd6+ Bg6 82. Qe7 Nd5 83. Qe6 Nf6 84. Kg1 Be8 85. Qf5 Bd7 86. Qe5 Kg6 87. +Kf1 Bf8 88. Kg1 Ba3 89. Qc7 Bb4 90. Qe5 Bd2 91. Qd4 Bf4 92. Qb6 Be8 93. Kf1 Bf7 +94. Kg1 Kf5 95. Qa7 Be6 96. Kf1 Kg6 97. Qb6 Bd7 98. Kg1 Ba4 99. Qe6 Bb5 100. Qb6 +Bc4 101. Qd4 Be6 102. Kf1 Bf5 103. Kg1 g4 104. hxg4 Bxg4 105. Qc3 Bf5 106. Qb3 +Bg5 107. Kf1 Kh6 108. Kg1 Kg6 109. Kf1 Bd7 110. Kg1 Be8 111. Qc3 Bf7 112. Qe5 +Bd5 113. Qc3 Be6 114. Qe5 Bd7 115. Qc3 Bf5 116. Qb3 Nh5 117. Qg8+ Ng7 118. Qb3 +Ne6 119. Kh2 Bf6 120. Kg1 Bg7 121. Kf1 Ng5 122. Qb6+ Bf6 123. Kg1 h3 124. gxh3 +Bxh3 125. Qd6 Bf5 126. Kg2 Nf3 127. Qd5 Kg5 128. Qg8+ Bg6 129. Qd5+ Be5 130. +Qd8+ Kh5 131. Qd5 Bf5 132. Qf7+ Kg4 133. Qg8+ Ng5 134. Qc4 Bf4 135. Qg8 Be6 136. +Qg7 Bf7 137. Qd4 Kf5 138. Qc5+ Be5 139. Qf8 Kg6 140. Qc5 Bf6 141. Qd6 Bc4 142. +Qc6 Be6 143. Qd6 Bg4 144. Qd5 Bf3+ 145. Kf1 Nf7 146. Kg1 Ne5 147. Qg8+ Kf5 148. +Qc8+ Kg5 149. Qg8+ Ng6 150. Qd5+ Kh6 151. Qe6 Be5 152. Qf5 Bf4 153. Qf6 Bg5 154. +Qe6 Kg7 155. Qd7+ Ne7 156. Qe6 Bf6 157. Kf1 Kg6 158. Kg1 Nf5 159. Qg8+ Kh5 160. +Qf7+ Kg5 161. Qg8+ Kf4 162. Qb8+ Kg4 163. Qg8+ Bg5 164. Qc8 Bf6 165. Qg8+ Kf4 +166. Qb8+ Be5 167. Qb4 Nd4 168. Qf8+ Kg5 169. Qg8+ Kh6 170. Qf8+ Bg7 171. Qd6+ +Kh5 172. Qh2+ Kg5 173. Qg3+ Bg4 174. Qe3+ Kf5 1/2-1/2 diff --git a/cartesi-python-chess-cartesi-img/chess-game-part.pgn b/cartesi-python-chess-cartesi-img/chess-game-part.pgn new file mode 100644 index 0000000..1b881ff --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess-game-part.pgn @@ -0,0 +1,28 @@ +[Event "Grand Slam Final Masters"] +[Site "Bilbao ESP"] +[Date "2010.10.11"] +[Round "3"] +[White "Shirov, Alexei"] +[Black "Carlsen, Magnus"] +[Result "1/2-1/2"] +[WhiteTitle "GM"] +[BlackTitle "GM"] +[WhiteElo "2749"] +[BlackElo "2826"] +[ECO "C95"] +[Opening "Ruy Lopez"] +[Variation "closed, Breyer, Borisenko variation"] +[WhiteFideId "2209390"] +[BlackFideId "1503014"] +[EventDate "2010.10.09"] +[EventType "DRR"] + +1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 +O-O 9. h3 Nb8 10. d4 Nbd7 11. Nbd2 Bb7 12. Bc2 Re8 13. a4 Bf8 14. Bd3 c6 15. b4 +Nb6 16. axb5 cxb5 17. d5 Rc8 18. Bb2 Nh5 19. Bf1 f5 20. Nxe5 Rxe5 21. c4 Nf6 22. +Bxe5 dxe5 23. c5 fxe4 24. d6 Na4 25. Nc4 Nxc5 26. bxc5 Rxc5 27. Qb3 Bd5 28. Qb4 +Rxc4 29. Bxc4 bxc4 30. Rxa6 Qc8 31. Qa5 c3 32. d7 Qxd7 33. Qxc3 Qb7 34. Rea1 Qb8 +35. Ra7 h6 36. Rc7 Qb6 37. Qxe5 Bd6 38. Rc8+ Kf7 39. Qc3 Qb7 40. Rca8 Qxa8 41. +Rxa8 Bxa8 42. Qd4 Bd5 43. Qa7+ Be7 44. Qc7 h5 45. Qe5 g6 46. Kh2 Be6 47. Kg1 Bf5 +48. Kf1 Ne8 49. Kg1 Ng7 50. Qd5+ Ne6 51. Qb7 h4 52. Kh2 Nf4 53. Kg1 Nd3 54. Kf1 +Nc5 55. Qd5+ Be6 56. Qe5 Bf5 57. Qd5+ Ne6 58. Qb7 Kf8 59. Qa8+ Bd8 60. Qa7 Bf6 diff --git a/cartesi-python-chess-cartesi-img/chess-game.pgn b/cartesi-python-chess-cartesi-img/chess-game.pgn new file mode 100644 index 0000000..edb6f56 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess-game.pgn @@ -0,0 +1,47 @@ +[Event "Grand Slam Final Masters"] +[Site "Bilbao ESP"] +[Date "2010.10.11"] +[Round "3"] +[White "Shirov, Alexei"] +[Black "Carlsen, Magnus"] +[Result "1/2-1/2"] +[WhiteTitle "GM"] +[BlackTitle "GM"] +[WhiteElo "2749"] +[BlackElo "2826"] +[ECO "C95"] +[Opening "Ruy Lopez"] +[Variation "closed, Breyer, Borisenko variation"] +[WhiteFideId "2209390"] +[BlackFideId "1503014"] +[EventDate "2010.10.09"] +[EventType "DRR"] + +1. e4 e5 2. Nf3 Nc8 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 +O-O 9. h3 Nb8 10. d4 Nbd7 11. Nbd2 Bb7 12. Bc2 Re8 13. a4 Bf8 14. Bd3 c6 15. b4 +Nb6 16. axb5 cxb5 17. d5 Rc8 18. Bb2 Nh5 19. Bf1 f5 20. Nxe5 Rxe5 21. c4 Nf6 22. +Bxe5 dxe5 23. c5 fxe4 24. d6 Na4 25. Nc4 Nxc5 26. bxc5 Rxc5 27. Qb3 Bd5 28. Qb4 +Rxc4 29. Bxc4 bxc4 30. Rxa6 Qc8 31. Qa5 c3 32. d7 Qxd7 33. Qxc3 Qb7 34. Rea1 Qb8 +35. Ra7 h6 36. Rc7 Qb6 37. Qxe5 Bd6 38. Rc8+ Kf7 39. Qc3 Qb7 40. Rca8 Qxa8 41. +Rxa8 Bxa8 42. Qd4 Bd5 43. Qa7+ Be7 44. Qc7 h5 45. Qe5 g6 46. Kh2 Be6 47. Kg1 Bf5 +48. Kf1 Ne8 49. Kg1 Ng7 50. Qd5+ Ne6 51. Qb7 h4 52. Kh2 Nf4 53. Kg1 Nd3 54. Kf1 +Nc5 55. Qd5+ Be6 56. Qe5 Bf5 57. Qd5+ Ne6 58. Qb7 Kf8 59. Qa8+ Bd8 60. Qa7 Bf6 +61. Qb7 Kg8 62. Qa7 Bg5 63. Ke2 Nf4+ 64. Kf1 Nh5 65. Ke2 Kf8 66. Kf1 Bf6 67. Qc7 +Ng7 68. Qa7 Ne6 69. Qb7 Nd8 70. Qa7 Nf7 71. Kg1 Kg7 72. Kf1 Be5 73. Qe7 g5 74. +Kg1 Bf6 75. Qb7 Kg6 76. Qd5 Ne5 77. Qg8+ Bg7 78. Qe8+ Kh6 79. Qe7 Nd3 80. Kf1 +Nf4 81. Qd6+ Bg6 82. Qe7 Nd5 83. Qe6 Nf6 84. Kg1 Be8 85. Qf5 Bd7 86. Qe5 Kg6 87. +Kf1 Bf8 88. Kg1 Ba3 89. Qc7 Bb4 90. Qe5 Bd2 91. Qd4 Bf4 92. Qb6 Be8 93. Kf1 Bf7 +94. Kg1 Kf5 95. Qa7 Be6 96. Kf1 Kg6 97. Qb6 Bd7 98. Kg1 Ba4 99. Qe6 Bb5 100. Qb6 +Bc4 101. Qd4 Be6 102. Kf1 Bf5 103. Kg1 g4 104. hxg4 Bxg4 105. Qc3 Bf5 106. Qb3 +Bg5 107. Kf1 Kh6 108. Kg1 Kg6 109. Kf1 Bd7 110. Kg1 Be8 111. Qc3 Bf7 112. Qe5 +Bd5 113. Qc3 Be6 114. Qe5 Bd7 115. Qc3 Bf5 116. Qb3 Nh5 117. Qg8+ Ng7 118. Qb3 +Ne6 119. Kh2 Bf6 120. Kg1 Bg7 121. Kf1 Ng5 122. Qb6+ Bf6 123. Kg1 h3 124. gxh3 +Bxh3 125. Qd6 Bf5 126. Kg2 Nf3 127. Qd5 Kg5 128. Qg8+ Bg6 129. Qd5+ Be5 130. +Qd8+ Kh5 131. Qd5 Bf5 132. Qf7+ Kg4 133. Qg8+ Ng5 134. Qc4 Bf4 135. Qg8 Be6 136. +Qg7 Bf7 137. Qd4 Kf5 138. Qc5+ Be5 139. Qf8 Kg6 140. Qc5 Bf6 141. Qd6 Bc4 142. +Qc6 Be6 143. Qd6 Bg4 144. Qd5 Bf3+ 145. Kf1 Nf7 146. Kg1 Ne5 147. Qg8+ Kf5 148. +Qc8+ Kg5 149. Qg8+ Ng6 150. Qd5+ Kh6 151. Qe6 Be5 152. Qf5 Bf4 153. Qf6 Bg5 154. +Qe6 Kg7 155. Qd7+ Ne7 156. Qe6 Bf6 157. Kf1 Kg6 158. Kg1 Nf5 159. Qg8+ Kh5 160. +Qf7+ Kg5 161. Qg8+ Kf4 162. Qb8+ Kg4 163. Qg8+ Bg5 164. Qc8 Bf6 165. Qg8+ Kf4 +166. Qb8+ Be5 167. Qb4 Nd4 168. Qf8+ Kg5 169. Qg8+ Kh6 170. Qf8+ Bg7 171. Qd6+ +Kh5 172. Qh2+ Kg5 173. Qg3+ Bg4 174. Qe3+ Kf5 1/2-1/2 diff --git a/cartesi-python-chess-cartesi-img/chess/__init__.py b/cartesi-python-chess-cartesi-img/chess/__init__.py new file mode 100644 index 0000000..3590338 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/__init__.py @@ -0,0 +1,4091 @@ +# This file is part of the python-chess library. +# Copyright (C) 2012-2021 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 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__ = "1.7.0" + +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 + +try: + from typing import Literal + _EnPassantSpec = Literal["legal", "fen", "xfen"] +except ImportError: + # Before Python 3.8. + _EnPassantSpec = str # type: ignore + + +Color = bool +COLORS = [WHITE, BLACK] = [True, False] +COLOR_NAMES = ["black", "white"] + +PieceType = int +PIECE_TYPES = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING] = range(1, 7) +PIECE_SYMBOLS = [None, "p", "n", "b", "r", "q", "k"] +PIECE_NAMES = [None, "pawn", "knight", "bishop", "rook", "queen", "king"] + +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": "♖", "r": "♜", + "N": "♘", "n": "♞", + "B": "♗", "b": "♝", + "Q": "♕", "q": "♛", + "K": "♔", "k": "♚", + "P": "♙", "p": "♟", +} + +FILE_NAMES = ["a", "b", "c", "d", "e", "f", "g", "h"] + +RANK_NAMES = ["1", "2", "3", "4", "5", "6", "7", "8"] + +STARTING_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" +"""The FEN for the standard chess starting position.""" + +STARTING_BOARD_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" +"""The board part of the FEN for the standard chess starting position.""" + + +class Status(enum.IntFlag): + VALID = 0 + NO_WHITE_KING = 1 << 0 + NO_BLACK_KING = 1 << 1 + TOO_MANY_KINGS = 1 << 2 + TOO_MANY_WHITE_PAWNS = 1 << 3 + TOO_MANY_BLACK_PAWNS = 1 << 4 + PAWNS_ON_BACKRANK = 1 << 5 + TOO_MANY_WHITE_PIECES = 1 << 6 + TOO_MANY_BLACK_PIECES = 1 << 7 + BAD_CASTLING_RIGHTS = 1 << 8 + INVALID_EP_SQUARE = 1 << 9 + OPPOSITE_CHECK = 1 << 10 + EMPTY = 1 << 11 + RACE_CHECK = 1 << 12 + RACE_OVER = 1 << 13 + RACE_MATERIAL = 1 << 14 + TOO_MANY_CHECKERS = 1 << 15 + IMPOSSIBLE_CHECK = 1 << 16 + +STATUS_VALID = Status.VALID +STATUS_NO_WHITE_KING = Status.NO_WHITE_KING +STATUS_NO_BLACK_KING = Status.NO_BLACK_KING +STATUS_TOO_MANY_KINGS = Status.TOO_MANY_KINGS +STATUS_TOO_MANY_WHITE_PAWNS = Status.TOO_MANY_WHITE_PAWNS +STATUS_TOO_MANY_BLACK_PAWNS = Status.TOO_MANY_BLACK_PAWNS +STATUS_PAWNS_ON_BACKRANK = Status.PAWNS_ON_BACKRANK +STATUS_TOO_MANY_WHITE_PIECES = Status.TOO_MANY_WHITE_PIECES +STATUS_TOO_MANY_BLACK_PIECES = Status.TOO_MANY_BLACK_PIECES +STATUS_BAD_CASTLING_RIGHTS = Status.BAD_CASTLING_RIGHTS +STATUS_INVALID_EP_SQUARE = Status.INVALID_EP_SQUARE +STATUS_OPPOSITE_CHECK = Status.OPPOSITE_CHECK +STATUS_EMPTY = Status.EMPTY +STATUS_RACE_CHECK = Status.RACE_CHECK +STATUS_RACE_OVER = Status.RACE_OVER +STATUS_RACE_MATERIAL = Status.RACE_MATERIAL +STATUS_TOO_MANY_CHECKERS = Status.TOO_MANY_CHECKERS +STATUS_IMPOSSIBLE_CHECK = Status.IMPOSSIBLE_CHECK + + +class Termination(enum.Enum): + """Enum with reasons for a game to be over.""" + + CHECKMATE = enum.auto() + """See :func:`chess.Board.is_checkmate()`.""" + STALEMATE = enum.auto() + """See :func:`chess.Board.is_stalemate()`.""" + INSUFFICIENT_MATERIAL = enum.auto() + """See :func:`chess.Board.is_insufficient_material()`.""" + SEVENTYFIVE_MOVES = enum.auto() + """See :func:`chess.Board.is_seventyfive_moves()`.""" + FIVEFOLD_REPETITION = enum.auto() + """See :func:`chess.Board.is_fivefold_repetition()`.""" + FIFTY_MOVES = enum.auto() + """See :func:`chess.Board.can_claim_fifty_moves()`.""" + THREEFOLD_REPETITION = enum.auto() + """See :func:`chess.Board.can_claim_threefold_repetition()`.""" + VARIANT_WIN = enum.auto() + """See :func:`chess.Board.is_variant_win()`.""" + VARIANT_LOSS = enum.auto() + """See :func:`chess.Board.is_variant_loss()`.""" + VARIANT_DRAW = enum.auto() + """See :func:`chess.Board.is_variant_draw()`.""" + +@dataclasses.dataclass +class Outcome: + """ + Information about the outcome of an ended game, usually obtained from + :func:`chess.Board.outcome()`. + """ + + termination: Termination + """The reason for the game to have ended.""" + + winner: Optional[Color] + """The winning color or ``None`` if drawn.""" + + def result(self) -> str: + """Returns ``1-0``, ``0-1`` or ``1/2-1/2``.""" + return "1/2-1/2" if self.winner is None else ("1-0" if self.winner else "0-1") + + +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) + +SQUARE_NAMES = [f + r for r in RANK_NAMES for f in FILE_NAMES] + +def parse_square(name: str) -> Square: + """ + Gets the square index for the given square *name* + (e.g., ``a1`` returns ``0``). + + :raises: :exc:`ValueError` if the square name is invalid. + """ + return SQUARE_NAMES.index(name) + +def square_name(square: Square) -> str: + """Gets the name of the square, like ``a3``.""" + return SQUARE_NAMES[square] + +def square(file_index: int, rank_index: int) -> Square: + """Gets a square number by file and rank index.""" + return rank_index * 8 + file_index + +def square_file(square: Square) -> int: + """Gets the file index of the square where ``0`` is the a-file.""" + return square & 7 + +def square_rank(square: Square) -> int: + """Gets the rank index of the square where ``0`` is the first rank.""" + return square >> 3 + +def square_distance(a: Square, b: Square) -> int: + """ + Gets the distance (i.e., the number of king steps) from square *a* to *b*. + """ + return max(abs(square_file(a) - square_file(b)), abs(square_rank(a) - square_rank(b))) + +def square_mirror(square: 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 + + +def lsb(bb: Bitboard) -> int: + return (bb & -bb).bit_length() - 1 + +def scan_forward(bb: Bitboard) -> Iterator[Square]: + while bb: + r = bb & -bb + yield r.bit_length() - 1 + bb ^= r + +def msb(bb: Bitboard) -> int: + return bb.bit_length() - 1 + +def scan_reversed(bb: Bitboard) -> Iterator[Square]: + while bb: + r = bb.bit_length() - 1 + yield r + bb ^= BB_SQUARES[r] + +# Python 3.10 or fallback. +popcount: Callable[[Bitboard], int] = getattr(int, "bit_count", lambda bb: bin(bb).count("1")) + +def flip_vertical(bb: Bitboard) -> Bitboard: + # https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipVertically + bb = ((bb >> 8) & 0x00ff_00ff_00ff_00ff) | ((bb & 0x00ff_00ff_00ff_00ff) << 8) + bb = ((bb >> 16) & 0x0000_ffff_0000_ffff) | ((bb & 0x0000_ffff_0000_ffff) << 16) + bb = (bb >> 32) | ((bb & 0x0000_0000_ffff_ffff) << 32) + return bb + +def flip_horizontal(bb: Bitboard) -> Bitboard: + # https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#MirrorHorizontally + bb = ((bb >> 1) & 0x5555_5555_5555_5555) | ((bb & 0x5555_5555_5555_5555) << 1) + bb = ((bb >> 2) & 0x3333_3333_3333_3333) | ((bb & 0x3333_3333_3333_3333) << 2) + bb = ((bb >> 4) & 0x0f0f_0f0f_0f0f_0f0f) | ((bb & 0x0f0f_0f0f_0f0f_0f0f) << 4) + return bb + +def flip_diagonal(bb: Bitboard) -> Bitboard: + # https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipabouttheDiagonal + t = (bb ^ (bb << 28)) & 0x0f0f_0f0f_0000_0000 + bb = bb ^ t ^ (t >> 28) + t = (bb ^ (bb << 14)) & 0x3333_0000_3333_0000 + bb = bb ^ t ^ (t >> 14) + t = (bb ^ (bb << 7)) & 0x5500_5500_5500_5500 + bb = bb ^ t ^ (t >> 7) + return bb + +def flip_anti_diagonal(bb: Bitboard) -> Bitboard: + # https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipabouttheAntidiagonal + t = bb ^ (bb << 36) + bb = bb ^ ((t ^ (bb >> 36)) & 0xf0f0_f0f0_0f0f_0f0f) + t = (bb ^ (bb << 18)) & 0xcccc_0000_cccc_0000 + bb = bb ^ t ^ (t >> 18) + t = (bb ^ (bb << 9)) & 0xaa00_aa00_aa00_aa00 + bb = bb ^ t ^ (t >> 9) + return bb + + +def shift_down(b: Bitboard) -> Bitboard: + return b >> 8 + +def shift_2_down(b: Bitboard) -> Bitboard: + return b >> 16 + +def shift_up(b: Bitboard) -> Bitboard: + return (b << 8) & BB_ALL + +def shift_2_up(b: Bitboard) -> Bitboard: + return (b << 16) & BB_ALL + +def shift_right(b: Bitboard) -> Bitboard: + return (b << 1) & ~BB_FILE_A & BB_ALL + +def shift_2_right(b: Bitboard) -> Bitboard: + return (b << 2) & ~BB_FILE_A & ~BB_FILE_B & BB_ALL + +def shift_left(b: Bitboard) -> Bitboard: + return (b >> 1) & ~BB_FILE_H + +def shift_2_left(b: Bitboard) -> Bitboard: + return (b >> 2) & ~BB_FILE_G & ~BB_FILE_H + +def shift_up_left(b: Bitboard) -> Bitboard: + return (b << 7) & ~BB_FILE_H & BB_ALL + +def shift_up_right(b: Bitboard) -> Bitboard: + return (b << 9) & ~BB_FILE_A & BB_ALL + +def shift_down_left(b: Bitboard) -> Bitboard: + return (b >> 9) & ~BB_FILE_H + +def shift_down_right(b: Bitboard) -> Bitboard: + return (b >> 7) & ~BB_FILE_A + + +def _sliding_attacks(square: Square, occupied: Bitboard, deltas: Iterable[int]) -> Bitboard: + attacks = BB_EMPTY + + for delta in deltas: + sq = square + + while True: + sq += delta + if not (0 <= sq < 64) or square_distance(sq, sq - delta) > 2: + break + + attacks |= BB_SQUARES[sq] + + if occupied & BB_SQUARES[sq]: + break + + return attacks + +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]]] + + +def _edges(square: Square) -> Bitboard: + return (((BB_RANK_1 | BB_RANK_8) & ~BB_RANKS[square_rank(square)]) | + ((BB_FILE_A | BB_FILE_H) & ~BB_FILES[square_file(square)])) + +def _carry_rippler(mask: Bitboard) -> Iterator[Bitboard]: + # Carry-Rippler trick to iterate subsets of mask. + subset = BB_EMPTY + while True: + yield subset + subset = (subset - mask) & mask + if not subset: + break + +def _attack_table(deltas: List[int]) -> Tuple[List[Bitboard], List[Dict[Bitboard, Bitboard]]]: + mask_table = [] + attack_table = [] + + for square in SQUARES: + attacks = {} + + mask = _sliding_attacks(square, 0, deltas) & ~_edges(square) + for subset in _carry_rippler(mask): + attacks[subset] = _sliding_attacks(square, subset, deltas) + + attack_table.append(attacks) + mask_table.append(mask) + + return mask_table, attack_table + +BB_DIAG_MASKS, BB_DIAG_ATTACKS = _attack_table([-9, -7, 7, 9]) +BB_FILE_MASKS, BB_FILE_ATTACKS = _attack_table([-8, 8]) +BB_RANK_MASKS, BB_RANK_ATTACKS = _attack_table([-1, 1]) + + +def _rays() -> List[List[Bitboard]]: + rays = [] + for a, bb_a in enumerate(BB_SQUARES): + rays_row = [] + 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) + elif BB_RANK_ATTACKS[a][0] & bb_b: + rays_row.append(BB_RANK_ATTACKS[a][0] | bb_a) + elif BB_FILE_ATTACKS[a][0] & bb_b: + rays_row.append(BB_FILE_ATTACKS[a][0] | bb_a) + else: + rays_row.append(BB_EMPTY) + rays.append(rays_row) + return rays + +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") + +FEN_CASTLING_REGEX = re.compile(r"^(?:-|[KQABCDEFGH]{0,2}[kqabcdefgh]{0,2})\Z") + + +@dataclasses.dataclass +class Piece: + """A piece with type and color.""" + + piece_type: PieceType + """The piece type.""" + + color: Color + """The piece color.""" + + def symbol(self) -> str: + """ + Gets the symbol ``P``, ``N``, ``B``, ``R``, ``Q`` or ``K`` for white + pieces or the lower-case variants for the black pieces. + """ + symbol = piece_symbol(self.piece_type) + return symbol.upper() if self.color else symbol + + def unicode_symbol(self, *, invert_color: bool = False) -> str: + """ + Gets the Unicode character for the piece. + """ + symbol = self.symbol().swapcase() if invert_color else self.symbol() + return UNICODE_PIECE_SYMBOLS[symbol] + + def __hash__(self) -> int: + return self.piece_type + (-1 if self.color else 5) + + def __repr__(self) -> str: + return f"Piece.from_symbol({self.symbol()!r})" + + def __str__(self) -> str: + return self.symbol() + + def _repr_svg_(self) -> str: + import chess.svg + return chess.svg.piece(self, size=45) + + @classmethod + def from_symbol(cls, symbol: str) -> Piece: + """ + Creates a :class:`~chess.Piece` instance from a piece symbol. + + :raises: :exc:`ValueError` if the symbol is invalid. + """ + 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 + piece type. + + Drops and null moves are supported. + """ + + 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 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). + + The UCI representation of a null move is ``0000``. + """ + if self.drop: + return piece_symbol(self.drop).upper() + "@" + SQUARE_NAMES[self.to_square] + elif self.promotion: + return SQUARE_NAMES[self.from_square] + SQUARE_NAMES[self.to_square] + piece_symbol(self.promotion) + elif self: + return SQUARE_NAMES[self.from_square] + SQUARE_NAMES[self.to_square] + else: + return "0000" + + def xboard(self) -> str: + return self.uci() if self else "@@@@" + + def __bool__(self) -> bool: + return bool(self.from_square or self.to_square or self.promotion or self.drop) + + def __repr__(self) -> str: + return f"Move.from_uci({self.uci()!r})" + + def __str__(self) -> str: + return self.uci() + + @classmethod + def from_uci(cls, uci: str) -> Move: + """ + Parses a UCI string. + + :raises: :exc:`ValueError` 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:]) + 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}") + return cls(from_square, to_square, promotion=promotion) + else: + raise ValueError(f"expected uci string to be of length 4 or 5: {uci!r}") + + @classmethod + def null(cls) -> Move: + """ + Gets a null move. + + A null move just passes the turn to the other side (and possibly + forfeits en passant capturing). Null moves evaluate to ``False`` in + boolean contexts. + + >>> import chess + >>> + >>> bool(chess.Move.null()) + False + """ + return cls(0, 0) + + +BaseBoardT = TypeVar("BaseBoardT", bound="BaseBoard") + +class BaseBoard: + """ + A board representing the position of chess pieces. See + :class:`~chess.Board` for a full board with move generation. + + The board is initialized with the standard chess starting position, unless + otherwise specified in the optional *board_fen* argument. If *board_fen* + is ``None``, an empty board is created. + """ + + def __init__(self, board_fen: Optional[str] = STARTING_BOARD_FEN) -> None: + self.occupied_co = [BB_EMPTY, BB_EMPTY] + + if board_fen is None: + self._clear_board() + elif board_fen == STARTING_BOARD_FEN: + self._reset_board() + else: + self._set_board_fen(board_fen) + + def _reset_board(self) -> None: + self.pawns = BB_RANK_2 | BB_RANK_7 + self.knights = BB_B1 | BB_G1 | BB_B8 | BB_G8 + self.bishops = BB_C1 | BB_F1 | BB_C8 | BB_F8 + self.rooks = BB_CORNERS + self.queens = BB_D1 | BB_D8 + self.kings = BB_E1 | BB_E8 + + self.promoted = BB_EMPTY + + self.occupied_co[WHITE] = BB_RANK_1 | BB_RANK_2 + self.occupied_co[BLACK] = BB_RANK_7 | BB_RANK_8 + self.occupied = BB_RANK_1 | BB_RANK_2 | BB_RANK_7 | BB_RANK_8 + + def reset_board(self) -> None: + """Resets pieces to the starting position.""" + self._reset_board() + + def _clear_board(self) -> None: + self.pawns = BB_EMPTY + self.knights = BB_EMPTY + self.bishops = BB_EMPTY + self.rooks = BB_EMPTY + self.queens = BB_EMPTY + self.kings = BB_EMPTY + + self.promoted = BB_EMPTY + + self.occupied_co[WHITE] = BB_EMPTY + self.occupied_co[BLACK] = BB_EMPTY + self.occupied = BB_EMPTY + + def clear_board(self) -> None: + """Clears the board.""" + self._clear_board() + + def pieces_mask(self, piece_type: PieceType, color: Color) -> Bitboard: + if piece_type == PAWN: + bb = self.pawns + elif piece_type == KNIGHT: + bb = self.knights + elif piece_type == BISHOP: + bb = self.bishops + elif piece_type == ROOK: + bb = self.rooks + elif piece_type == QUEEN: + 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: + """ + Gets pieces of the given type and color. + + Returns a :class:`set of squares `. + """ + return SquareSet(self.pieces_mask(piece_type, color)) + + def piece_at(self, square: Square) -> Optional[Piece]: + """Gets the :class:`piece ` at the given square.""" + piece_type = self.piece_type_at(square) + if piece_type: + mask = BB_SQUARES[square] + color = bool(self.occupied_co[WHITE] & mask) + return Piece(piece_type, color) + else: + return None + + def piece_type_at(self, square: Square) -> Optional[PieceType]: + """Gets the piece type at the given square.""" + mask = BB_SQUARES[square] + + if not self.occupied & mask: + return None # Early return + elif self.pawns & mask: + return PAWN + elif self.knights & mask: + return KNIGHT + elif self.bishops & mask: + return BISHOP + elif self.rooks & mask: + return ROOK + elif self.queens & mask: + return QUEEN + else: + return KING + + def color_at(self, square: Square) -> Optional[Color]: + """Gets the color of the piece at the given square.""" + mask = BB_SQUARES[square] + if self.occupied_co[WHITE] & mask: + return WHITE + elif self.occupied_co[BLACK] & mask: + return BLACK + else: + return None + + def king(self, color: Color) -> Optional[Square]: + """ + Finds the king square of the given side. Returns ``None`` if there + is no king 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 + + def attacks_mask(self, square: Square) -> Bitboard: + bb_square = BB_SQUARES[square] + + if bb_square & self.pawns: + color = bool(bb_square & self.occupied_co[WHITE]) + return BB_PAWN_ATTACKS[color][square] + elif bb_square & self.knights: + return BB_KNIGHT_ATTACKS[square] + elif bb_square & self.kings: + return BB_KING_ATTACKS[square] + else: + attacks = 0 + if bb_square & self.bishops or bb_square & self.queens: + attacks = BB_DIAG_ATTACKS[square][BB_DIAG_MASKS[square] & self.occupied] + if bb_square & self.rooks or bb_square & self.queens: + attacks |= (BB_RANK_ATTACKS[square][BB_RANK_MASKS[square] & self.occupied] | + BB_FILE_ATTACKS[square][BB_FILE_MASKS[square] & self.occupied]) + return attacks + + def attacks(self, square: Square) -> SquareSet: + """ + Gets the set of attacked squares from the given square. + + There will be no attacks if the square is empty. Pinned pieces are + still attacking other squares. + + Returns a :class:`set of squares `. + """ + return SquareSet(self.attacks_mask(square)) + + def _attackers_mask(self, color: Color, square: Square, occupied: Bitboard) -> Bitboard: + rank_pieces = BB_RANK_MASKS[square] & occupied + file_pieces = BB_FILE_MASKS[square] & occupied + diag_pieces = BB_DIAG_MASKS[square] & occupied + + queens_and_rooks = self.queens | self.rooks + queens_and_bishops = self.queens | self.bishops + + attackers = ( + (BB_KING_ATTACKS[square] & self.kings) | + (BB_KNIGHT_ATTACKS[square] & self.knights) | + (BB_RANK_ATTACKS[square][rank_pieces] & queens_and_rooks) | + (BB_FILE_ATTACKS[square][file_pieces] & queens_and_rooks) | + (BB_DIAG_ATTACKS[square][diag_pieces] & queens_and_bishops) | + (BB_PAWN_ATTACKS[not color][square] & self.pawns)) + + return attackers & self.occupied_co[color] + + def attackers_mask(self, color: Color, square: Square) -> Bitboard: + return self._attackers_mask(color, square, self.occupied) + + def is_attacked_by(self, color: Color, square: Square) -> 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. + """ + return bool(self.attackers_mask(color, square)) + + def attackers(self, color: Color, square: Square) -> SquareSet: + """ + Gets the set of attackers of the given color for the given square. + + Pinned pieces still count as attackers. + + Returns a :class:`set of squares `. + """ + return SquareSet(self.attackers_mask(color, square)) + + def pin_mask(self, color: Color, square: Square) -> Bitboard: + king = self.king(color) + if king is None: + return BB_ALL + + square_mask = BB_SQUARES[square] + + for attacks, sliders in [(BB_FILE_ATTACKS, self.rooks | self.queens), + (BB_RANK_ATTACKS, self.rooks | self.queens), + (BB_DIAG_ATTACKS, self.bishops | self.queens)]: + rays = attacks[king][0] + if rays & square_mask: + snipers = rays & sliders & self.occupied_co[not color] + for sniper in scan_reversed(snipers): + if between(sniper, king) & (self.occupied | square_mask) == square_mask: + return ray(king, sniper) + + break + + return BB_ALL + + def pin(self, color: Color, square: Square) -> SquareSet: + """ + Detects an absolute pin (and its direction) of the given square to + the king of the given color. + + >>> import chess + >>> + >>> board = chess.Board("rnb1k2r/ppp2ppp/5n2/3q4/1b1P4/2N5/PP3PPP/R1BQKBNR w KQkq - 3 7") + >>> board.is_pinned(chess.WHITE, chess.C3) + True + >>> direction = board.pin(chess.WHITE, chess.C3) + >>> direction + SquareSet(0x0000_0001_0204_0810) + >>> print(direction) + . . . . . . . . + . . . . . . . . + . . . . . . . . + 1 . . . . . . . + . 1 . . . . . . + . . 1 . . . . . + . . . 1 . . . . + . . . . 1 . . . + + Returns a :class:`set of squares ` that mask the rank, + file or diagonal of the pin. If there is no pin, then a mask of the + entire board is returned. + """ + return SquareSet(self.pin_mask(color, square)) + + def is_pinned(self, color: Color, square: Square) -> bool: + """ + Detects if the given square is pinned to the king of the given color. + """ + return self.pin_mask(color, square) != BB_ALL + + def _remove_piece_at(self, square: Square) -> Optional[PieceType]: + piece_type = self.piece_type_at(square) + mask = BB_SQUARES[square] + + if piece_type == PAWN: + self.pawns ^= mask + elif piece_type == KNIGHT: + self.knights ^= mask + elif piece_type == BISHOP: + self.bishops ^= mask + elif piece_type == ROOK: + self.rooks ^= mask + elif piece_type == QUEEN: + self.queens ^= mask + elif piece_type == KING: + self.kings ^= mask + else: + return None + + self.occupied ^= mask + self.occupied_co[WHITE] &= ~mask + self.occupied_co[BLACK] &= ~mask + + self.promoted &= ~mask + + return piece_type + + 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. + """ + color = bool(self.occupied_co[WHITE] & BB_SQUARES[square]) + piece_type = self._remove_piece_at(square) + return Piece(piece_type, color) if piece_type else None + + def _set_piece_at(self, square: Square, piece_type: PieceType, color: Color, promoted: bool = False) -> None: + self._remove_piece_at(square) + + mask = BB_SQUARES[square] + + if piece_type == PAWN: + self.pawns |= mask + elif piece_type == KNIGHT: + self.knights |= mask + elif piece_type == BISHOP: + self.bishops |= mask + elif piece_type == ROOK: + self.rooks |= mask + elif piece_type == QUEEN: + self.queens |= mask + elif piece_type == KING: + self.kings |= mask + else: + return + + self.occupied ^= mask + self.occupied_co[color] ^= mask + + if promoted: + self.promoted ^= mask + + def set_piece_at(self, square: Square, piece: Optional[Piece], promoted: bool = False) -> None: + """ + Sets a piece at the given square. + + An existing piece is replaced. Setting *piece* to ``None`` is + equivalent to :func:`~chess.Board.remove_piece_at()`. + """ + 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: + """ + Gets the board FEN (e.g., + ``rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR``). + """ + builder = [] + empty = 0 + + for square in SQUARES_180: + piece = self.piece_at(square) + + if not piece: + empty += 1 + else: + if empty: + builder.append(str(empty)) + empty = 0 + builder.append(piece.symbol()) + if promoted and BB_SQUARES[square] & self.promoted: + builder.append("~") + + if BB_SQUARES[square] & BB_FILE_H: + if empty: + builder.append(str(empty)) + empty = 0 + + if square != H1: + builder.append("/") + + return "".join(builder) + + def _set_board_fen(self, fen: str) -> None: + # Compatibility with set_fen(). + fen = fen.strip() + if " " in fen: + raise ValueError(f"expected position part of fen, got multiple parts: {fen!r}") + + # Ensure the FEN is valid. + rows = fen.split("/") + if len(rows) != 8: + raise ValueError(f"expected 8 rows in position part of fen: {fen!r}") + + # Validate each row. + for row in rows: + field_sum = 0 + previous_was_digit = False + previous_was_piece = False + + for c in row: + if c in ["1", "2", "3", "4", "5", "6", "7", "8"]: + if previous_was_digit: + raise ValueError(f"two subsequent digits in position part of fen: {fen!r}") + field_sum += int(c) + previous_was_digit = True + previous_was_piece = False + elif c == "~": + if not previous_was_piece: + raise ValueError(f"'~' not after piece in position part of fen: {fen!r}") + previous_was_digit = False + previous_was_piece = False + elif c.lower() in PIECE_SYMBOLS: + field_sum += 1 + previous_was_digit = False + previous_was_piece = True + else: + raise ValueError(f"invalid character in position part of fen: {fen!r}") + + if field_sum != 8: + raise ValueError(f"expected 8 columns per row in position part of fen: {fen!r}") + + # Clear the board. + self._clear_board() + + # Put pieces on the board. + square_index = 0 + for c in fen: + if c in ["1", "2", "3", "4", "5", "6", "7", "8"]: + square_index += int(c) + elif c.lower() in PIECE_SYMBOLS: + piece = Piece.from_symbol(c) + self._set_piece_at(SQUARES_180[square_index], piece.piece_type, piece.color) + square_index += 1 + elif c == "~": + self.promoted |= BB_SQUARES[SQUARES_180[square_index - 1]] + + def set_board_fen(self, fen: str) -> None: + """ + Parses *fen* and sets up the board, where *fen* is the board part of + a FEN. + + :raises: :exc:`ValueError` if syntactically invalid. + """ + self._set_board_fen(fen) + + 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 & mask): + result[square] = typing.cast(Piece, self.piece_at(square)) + return result + + def _set_piece_map(self, pieces: Mapping[Square, Piece]) -> None: + self._clear_board() + for square, piece in pieces.items(): + self._set_piece_at(square, piece.piece_type, piece.color) + + def set_piece_map(self, pieces: Mapping[Square, Piece]) -> None: + """ + Sets up the board from a dictionary of :class:`pieces ` + by square index. + """ + self._set_piece_map(pieces) + + def _set_chess960_pos(self, scharnagl: int) -> None: + if not 0 <= scharnagl <= 959: + raise ValueError(f"chess960 position index not 0 <= {scharnagl!r} <= 959") + + # See http://www.russellcottrell.com/Chess/Chess960.htm for + # a description of the algorithm. + n, bw = divmod(scharnagl, 4) + n, bb = divmod(n, 4) + n, q = divmod(n, 6) + + for n1 in range(0, 4): + n2 = n + (3 - n1) * (4 - n1) // 2 - 5 + if n1 < n2 and 1 <= n2 <= 4: + break + + # Bishops. + bw_file = bw * 2 + 1 + bb_file = bb * 2 + self.bishops = (BB_FILES[bw_file] | BB_FILES[bb_file]) & BB_BACKRANKS + + # Queens. + q_file = q + q_file += int(min(bw_file, bb_file) <= q_file) + q_file += int(max(bw_file, bb_file) <= q_file) + self.queens = BB_FILES[q_file] & BB_BACKRANKS + + used = [bw_file, bb_file, q_file] + + # Knights. + self.knights = BB_EMPTY + for i in range(0, 8): + if i not in used: + if n1 == 0 or n2 == 0: + self.knights |= BB_FILES[i] & BB_BACKRANKS + used.append(i) + n1 -= 1 + n2 -= 1 + + # RKR. + for i in range(0, 8): + if i not in used: + self.rooks = BB_FILES[i] & BB_BACKRANKS + used.append(i) + break + for i in range(1, 8): + if i not in used: + self.kings = BB_FILES[i] & BB_BACKRANKS + used.append(i) + break + for i in range(2, 8): + if i not in used: + self.rooks |= BB_FILES[i] & BB_BACKRANKS + break + + # Finalize. + self.pawns = BB_RANK_2 | BB_RANK_7 + self.occupied_co[WHITE] = BB_RANK_1 | BB_RANK_2 + self.occupied_co[BLACK] = BB_RANK_7 | BB_RANK_8 + self.occupied = BB_RANK_1 | BB_RANK_2 | BB_RANK_7 | BB_RANK_8 + self.promoted = BB_EMPTY + + 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(scharnagl) + + def chess960_pos(self) -> Optional[int]: + """ + Gets the Chess960 starting position index between 0 and 959, + or ``None``. + """ + if self.occupied_co[WHITE] != BB_RANK_1 | BB_RANK_2: + return None + if self.occupied_co[BLACK] != BB_RANK_7 | BB_RANK_8: + return None + if self.pawns != BB_RANK_2 | BB_RANK_7: + return None + if self.promoted: + return None + + # Piece counts. + brnqk = [self.bishops, self.rooks, self.knights, self.queens, self.kings] + if [popcount(pieces) for pieces in brnqk] != [4, 4, 4, 2, 2]: + return None + + # Symmetry. + if any((BB_RANK_1 & pieces) << 56 != BB_RANK_8 & pieces for pieces in brnqk): + return None + + # Algorithm from ChessX, src/database/bitboard.cpp, r2254. + x = self.bishops & (2 + 8 + 32 + 128) + if not x: + return None + bs1 = (lsb(x) - 1) // 2 + cc_pos = bs1 + x = self.bishops & (1 + 4 + 16 + 64) + if not x: + return None + bs2 = lsb(x) * 2 + cc_pos += bs2 + + q = 0 + qf = False + n0 = 0 + n1 = 0 + n0f = False + n1f = False + rf = 0 + n0s = [0, 4, 7, 9] + for square in range(A1, H1 + 1): + bb = BB_SQUARES[square] + if bb & self.queens: + qf = True + elif bb & self.rooks or bb & self.kings: + if bb & self.kings: + if rf != 1: + return None + else: + rf += 1 + + if not qf: + q += 1 + + if not n0f: + n0 += 1 + elif not n1f: + n1 += 1 + elif bb & self.knights: + if not qf: + q += 1 + + if not n0f: + n0f = True + elif not n1f: + n1f = True + + if n0 < 4 and n1f and qf: + cc_pos += q * 16 + krn = n0s[n0] + n1 + cc_pos += krn * 96 + return cc_pos + else: + return None + + def __repr__(self) -> str: + return f"{type(self).__name__}({self.board_fen()!r})" + + def __str__(self) -> str: + builder = [] + + for square in SQUARES_180: + piece = self.piece_at(square) + + if piece: + builder.append(piece.symbol()) + else: + builder.append(".") + + if BB_SQUARES[square] & BB_FILE_H: + if square != H1: + builder.append("\n") + else: + builder.append(" ") + + return "".join(builder) + + def unicode(self, *, invert_color: bool = False, borders: bool = False, empty_square: str = "⭘") -> str: + """ + Returns a string representation of the board with Unicode pieces. + Useful for pretty-printing to a terminal. + + :param invert_color: Invert color of the Unicode pieces. + :param borders: Show borders and a coordinate margin. + """ + builder = [] + for rank_index in range(7, -1, -1): + if borders: + builder.append(" ") + builder.append("-" * 17) + builder.append("\n") + + builder.append(RANK_NAMES[rank_index]) + builder.append(" ") + + for file_index in range(8): + square_index = square(file_index, rank_index) + + if borders: + builder.append("|") + elif file_index > 0: + builder.append(" ") + + piece = self.piece_at(square_index) + + if piece: + builder.append(piece.unicode_symbol(invert_color=invert_color)) + else: + builder.append(empty_square) + + if borders: + builder.append("|") + + if borders or rank_index > 0: + builder.append("\n") + + if borders: + builder.append(" ") + builder.append("-" * 17) + builder.append("\n") + builder.append(" a b c d e f g h") + + return "".join(builder) + + def _repr_svg_(self) -> str: + import chess.svg + return chess.svg.board(board=self, size=400) + + def __eq__(self, board: object) -> bool: + if isinstance(board, BaseBoard): + return ( + self.occupied == board.occupied and + self.occupied_co[WHITE] == board.occupied_co[WHITE] and + self.pawns == board.pawns and + self.knights == board.knights and + self.bishops == board.bishops and + self.rooks == board.rooks and + self.queens == board.queens and + self.kings == board.kings) + else: + return NotImplemented + + def apply_transform(self, f: Callable[[Bitboard], Bitboard]) -> None: + self.pawns = f(self.pawns) + self.knights = f(self.knights) + self.bishops = f(self.bishops) + self.rooks = f(self.rooks) + self.queens = f(self.queens) + self.kings = f(self.kings) + + self.occupied_co[WHITE] = f(self.occupied_co[WHITE]) + self.occupied_co[BLACK] = f(self.occupied_co[BLACK]) + self.occupied = f(self.occupied) + self.promoted = f(self.promoted) + + def transform(self: BaseBoardT, f: Callable[[Bitboard], Bitboard]) -> BaseBoardT: + """ + Returns a transformed copy of the board by applying a bitboard + transformation function. + + Available transformations include :func:`chess.flip_vertical()`, + :func:`chess.flip_horizontal()`, :func:`chess.flip_diagonal()`, + :func:`chess.flip_anti_diagonal()`, :func:`chess.shift_down()`, + :func:`chess.shift_up()`, :func:`chess.shift_left()`, and + :func:`chess.shift_right()`. + + Alternatively, :func:`~chess.BaseBoard.apply_transform()` can be used + to apply the transformation on the board. + """ + board = self.copy() + board.apply_transform(f) + return board + + def apply_mirror(self: BaseBoardT) -> 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: BaseBoardT) -> BaseBoardT: + """ + 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. + + Alternatively, :func:`~chess.BaseBoard.apply_mirror()` can be used + to mirror the board. + """ + board = self.copy() + board.apply_mirror() + return board + + def copy(self: BaseBoardT) -> BaseBoardT: + """Creates a copy of the board.""" + board = type(self)(None) + + board.pawns = self.pawns + board.knights = self.knights + board.bishops = self.bishops + board.rooks = self.rooks + board.queens = self.queens + board.kings = self.kings + + board.occupied_co[WHITE] = self.occupied_co[WHITE] + board.occupied_co[BLACK] = self.occupied_co[BLACK] + board.occupied = self.occupied + board.promoted = self.promoted + + return board + + def __copy__(self: BaseBoardT) -> BaseBoardT: + return self.copy() + + def __deepcopy__(self: BaseBoardT, memo: Dict[int, object]) -> BaseBoardT: + board = self.copy() + memo[id(self)] = board + return board + + @classmethod + def empty(cls: Type[BaseBoardT]) -> BaseBoardT: + """ + Creates a new empty board. Also see + :func:`~chess.BaseBoard.clear_board()`. + """ + return cls(None) + + @classmethod + def from_chess960_pos(cls: Type[BaseBoardT], scharnagl: int) -> BaseBoardT: + """ + Creates a new board, initialized with a Chess960 starting position. + + >>> import chess + >>> import random + >>> + >>> board = chess.Board.from_chess960_pos(random.randint(0, 959)) + """ + board = cls.empty() + board.set_chess960_pos(scharnagl) + return board + + +BoardT = TypeVar("BoardT", bound="Board") + +class _BoardState(Generic[BoardT]): + + def __init__(self, board: BoardT) -> None: + self.pawns = board.pawns + self.knights = board.knights + self.bishops = board.bishops + self.rooks = board.rooks + self.queens = board.queens + self.kings = board.kings + + self.occupied_w = board.occupied_co[WHITE] + self.occupied_b = board.occupied_co[BLACK] + self.occupied = board.occupied + + self.promoted = board.promoted + + self.turn = board.turn + self.castling_rights = board.castling_rights + self.ep_square = board.ep_square + self.halfmove_clock = board.halfmove_clock + self.fullmove_number = board.fullmove_number + + def restore(self, board: BoardT) -> None: + board.pawns = self.pawns + board.knights = self.knights + board.bishops = self.bishops + board.rooks = self.rooks + board.queens = self.queens + board.kings = self.kings + + board.occupied_co[WHITE] = self.occupied_w + board.occupied_co[BLACK] = self.occupied_b + board.occupied = self.occupied + + board.promoted = self.promoted + + board.turn = self.turn + board.castling_rights = self.castling_rights + board.ep_square = self.ep_square + board.halfmove_clock = self.halfmove_clock + board.fullmove_number = self.fullmove_number + +class Board(BaseBoard): + """ + A :class:`~chess.BaseBoard`, additional information representing + a chess position, and a :data:`move stack `. + + Provides :data:`move generation `, validation, + :func:`parsing `, attack generation, + :func:`game end detection `, + and the capability to :func:`make ` and + :func:`unmake ` moves. + + The board is initialized to the standard chess starting position, + unless otherwise specified in the optional *fen* argument. + If *fen* is ``None``, an empty board is created. + + Optionally supports *chess960*. In Chess960, castling moves are encoded + by a king move to the corresponding rook square. + Use :func:`chess.Board.from_chess960_pos()` to create a board with one + of the Chess960 starting positions. + + It's safe to set :data:`~Board.turn`, :data:`~Board.castling_rights`, + :data:`~Board.ep_square`, :data:`~Board.halfmove_clock` and + :data:`~Board.fullmove_number` directly. + + .. warning:: + It is possible to set up and work with invalid positions. In this + case, :class:`~chess.Board` implements a kind of "pseudo-chess" + (useful to gracefully handle errors or to implement chess variants). + Use :func:`~chess.Board.is_valid()` to detect invalid positions. + """ + + aliases: ClassVar[List[str]] = ["Standard", "Chess", "Classical", "Normal", "Illegal", "From Position"] + uci_variant: ClassVar[Optional[str]] = "chess" + xboard_variant: ClassVar[Optional[str]] = "normal" + starting_fen: ClassVar[str] = STARTING_FEN + + tbw_suffix: ClassVar[Optional[str]] = ".rtbw" + tbz_suffix: ClassVar[Optional[str]] = ".rtbz" + tbw_magic: ClassVar[Optional[bytes]] = b"\x71\xe8\x23\x5d" + tbz_magic: ClassVar[Optional[bytes]] = b"\xd7\x66\x0c\xa5" + pawnless_tbw_suffix: ClassVar[Optional[str]] = None + pawnless_tbz_suffix: ClassVar[Optional[str]] = None + pawnless_tbw_magic: ClassVar[Optional[bytes]] = None + pawnless_tbz_magic: ClassVar[Optional[bytes]] = None + connected_kings: ClassVar[bool] = False + one_king: ClassVar[bool] = True + captures_compulsory: ClassVar[bool] = False + + turn: Color + """The side to move (``chess.WHITE`` or ``chess.BLACK``).""" + + castling_rights: Bitboard + """ + Bitmask of the rooks with castling rights. + + To test for specific squares: + + >>> import chess + >>> + >>> board = chess.Board() + >>> bool(board.castling_rights & chess.BB_H1) # White can castle with the h1 rook + True + + To add a specific square: + + >>> board.castling_rights |= chess.BB_A1 + + Use :func:`~chess.Board.set_castling_fen()` to set multiple castling + rights. Also see :func:`~chess.Board.has_castling_rights()`, + :func:`~chess.Board.has_kingside_castling_rights()`, + :func:`~chess.Board.has_queenside_castling_rights()`, + :func:`~chess.Board.has_chess960_castling_rights()`, + :func:`~chess.Board.clean_castling_rights()`. + """ + + ep_square: Optional[Square] + """ + The potential en passant square on the third or sixth rank or ``None``. + + Use :func:`~chess.Board.has_legal_en_passant()` to test if en passant + capturing would actually be possible on the next move. + """ + + fullmove_number: int + """ + Counts move pairs. Starts at `1` and is incremented after every move + of the black side. + """ + + halfmove_clock: int + """The number of half-moves since the last capture or pawn move.""" + + promoted: Bitboard + """A bitmask of pieces that have been promoted.""" + + chess960: bool + """ + Whether the board is in Chess960 mode. In Chess960 castling moves are + represented as king moves to the corresponding rook square. + """ + + 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: BoardT, fen: Optional[str] = STARTING_FEN, *, chess960: bool = False) -> None: + BaseBoard.__init__(self, None) + + self.chess960 = chess960 + + self.ep_square = None + self.move_stack = [] + self._stack: List[_BoardState[BoardT]] = [] + + if fen is None: + self.clear() + elif fen == type(self).starting_fen: + self.reset() + else: + self.set_fen(fen) + + @property + def legal_moves(self) -> LegalMoveGenerator: + """ + A dynamic list of legal moves. + + >>> import chess + >>> + >>> board = chess.Board() + >>> board.legal_moves.count() + 20 + >>> bool(board.legal_moves) + True + >>> move = chess.Move.from_uci("g1f3") + >>> move in board.legal_moves + True + + Wraps :func:`~chess.Board.generate_legal_moves()` and + :func:`~chess.Board.is_legal()`. + """ + return LegalMoveGenerator(self) + + @property + def pseudo_legal_moves(self) -> PseudoLegalMoveGenerator: + """ + A dynamic list of pseudo-legal moves, much like the legal move list. + + Pseudo-legal moves might leave or put the king in check, but are + otherwise valid. Null moves are not pseudo-legal. Castling moves are + only included if they are completely legal. + + Wraps :func:`~chess.Board.generate_pseudo_legal_moves()` and + :func:`~chess.Board.is_pseudo_legal()`. + """ + return PseudoLegalMoveGenerator(self) + + def reset(self) -> None: + """Restores the starting position.""" + self.turn = WHITE + self.castling_rights = BB_CORNERS + self.ep_square = None + self.halfmove_clock = 0 + self.fullmove_number = 1 + + self.reset_board() + + def reset_board(self) -> None: + """ + Resets only pieces to the starting position. Use + :func:`~chess.Board.reset()` to fully restore the starting position + (including turn, castling rights, etc.). + """ + super().reset_board() + self.clear_stack() + + def clear(self) -> None: + """ + Clears the board. + + 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 + need to be put on the board. + """ + self.turn = WHITE + self.castling_rights = BB_EMPTY + self.ep_square = None + self.halfmove_clock = 0 + self.fullmove_number = 1 + + self.clear_board() + + def clear_board(self) -> None: + super().clear_board() + self.clear_stack() + + def clear_stack(self) -> None: + """Clears the move stack.""" + self.move_stack.clear() + self._stack.clear() + + def root(self: BoardT) -> BoardT: + """Returns a copy of the root position.""" + if self._stack: + board = type(self)(None, chess960=self.chess960) + self._stack[0].restore(board) + return board + else: + return self.copy(stack=False) + + def ply(self) -> int: + """ + Returns the number of half-moves since the start of the game, as + indicated by :data:`~chess.Board.fullmove_number` and + :data:`~chess.Board.turn`. + + If moves have been pushed from the beginning, this is usually equal to + ``len(board.move_stack)``. But note that a board can be set up with + arbitrary starting positions, and the stack can be cleared. + """ + return 2 * (self.fullmove_number - 1) + (self.turn == BLACK) + + def remove_piece_at(self, square: Square) -> Optional[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: + super().set_piece_at(square, piece, promoted=promoted) + self.clear_stack() + + def generate_pseudo_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: + our_pieces = self.occupied_co[self.turn] + + # Generate piece moves. + non_pawns = our_pieces & ~self.pawns & from_mask + for from_square in scan_reversed(non_pawns): + moves = self.attacks_mask(from_square) & ~our_pieces & to_mask + for to_square in scan_reversed(moves): + yield Move(from_square, to_square) + + # Generate castling moves. + if from_mask & self.kings: + yield from self.generate_castling_moves(from_mask, to_mask) + + # The remaining moves are all pawn moves. + pawns = self.pawns & self.occupied_co[self.turn] & from_mask + if not pawns: + return + + # Generate pawn captures. + capturers = pawns + for from_square in scan_reversed(capturers): + targets = ( + BB_PAWN_ATTACKS[self.turn][from_square] & + self.occupied_co[not self.turn] & to_mask) + + for to_square in scan_reversed(targets): + if square_rank(to_square) in [0, 7]: + yield Move(from_square, to_square, QUEEN) + yield Move(from_square, to_square, ROOK) + yield Move(from_square, to_square, BISHOP) + yield Move(from_square, to_square, KNIGHT) + else: + yield Move(from_square, to_square) + + # Prepare pawn advance generation. + if self.turn == WHITE: + single_moves = pawns << 8 & ~self.occupied + double_moves = single_moves << 8 & ~self.occupied & (BB_RANK_3 | BB_RANK_4) + else: + single_moves = pawns >> 8 & ~self.occupied + double_moves = single_moves >> 8 & ~self.occupied & (BB_RANK_6 | BB_RANK_5) + + single_moves &= to_mask + double_moves &= to_mask + + # Generate single pawn moves. + 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]: + yield Move(from_square, to_square, QUEEN) + yield Move(from_square, to_square, ROOK) + yield Move(from_square, to_square, BISHOP) + yield Move(from_square, to_square, KNIGHT) + else: + yield Move(from_square, to_square) + + # Generate double pawn moves. + for to_square in scan_reversed(double_moves): + from_square = to_square + (16 if self.turn == BLACK else -16) + yield Move(from_square, to_square) + + # Generate en passant captures. + if self.ep_square: + yield from self.generate_pseudo_legal_ep(from_mask, to_mask) + + def generate_pseudo_legal_ep(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: + if not self.ep_square or not BB_SQUARES[self.ep_square] & to_mask: + return + + if BB_SQUARES[self.ep_square] & self.occupied: + return + + capturers = ( + self.pawns & self.occupied_co[self.turn] & from_mask & + BB_PAWN_ATTACKS[not self.turn][self.ep_square] & + BB_RANKS[4 if self.turn else 3]) + + for capturer in scan_reversed(capturers): + yield Move(capturer, self.ep_square) + + def generate_pseudo_legal_captures(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: + return itertools.chain( + self.generate_pseudo_legal_moves(from_mask, to_mask & self.occupied_co[not self.turn]), + self.generate_pseudo_legal_ep(from_mask, to_mask)) + + def 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: + """ + Gets the pieces currently giving check. + + Returns a :class:`set of squares `. + """ + return SquareSet(self.checkers_mask()) + + def is_check(self) -> bool: + """Tests if the current side to move is in check.""" + return bool(self.checkers_mask()) + + 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. + """ + self.push(move) + try: + return self.is_check() + finally: + self.pop() + + def is_into_check(self, move: Move) -> bool: + king = self.king(self.turn) + if king is None: + return False + + # If already in check, look if it is an evasion. + checkers = self.attackers_mask(not self.turn, king) + if checkers and move not in self._generate_evasions(king, checkers, BB_SQUARES[move.from_square], BB_SQUARES[move.to_square]): + return True + + return not self._is_safe(king, self._slider_blockers(king), move) + + def was_into_check(self) -> bool: + king = self.king(not self.turn) + return king is not None and self.is_attacked_by(self.turn, king) + + def is_pseudo_legal(self, move: Move) -> bool: + # Null moves are not pseudo-legal. + if not move: + return False + + # Drops are not pseudo-legal. + if move.drop: + return False + + # Source square must not be vacant. + piece = self.piece_type_at(move.from_square) + if not piece: + return False + + # Get square masks. + from_mask = BB_SQUARES[move.from_square] + to_mask = BB_SQUARES[move.to_square] + + # Check turn. + if not self.occupied_co[self.turn] & from_mask: + return False + + # 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: + return False + elif self.turn == BLACK and square_rank(move.to_square) != 0: + 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 + + # Destination square can not be occupied. + if self.occupied_co[self.turn] & to_mask: + return False + + # Handle pawn moves. + if piece == PAWN: + return move in self.generate_pseudo_legal_moves(from_mask, to_mask) + + # Handle all other pieces. + return bool(self.attacks_mask(move.from_square) & to_mask) + + def is_legal(self, move: Move) -> bool: + 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: + """ + Checks if the game is over due to a special variant end condition. + + Note, for example, that stalemate is not considered a variant-specific + end condition (this method will return ``False``), yet it can have a + special **result** in suicide chess (any of + :func:`~chess.Board.is_variant_loss()`, + :func:`~chess.Board.is_variant_win()`, + :func:`~chess.Board.is_variant_draw()` might return ``True``). + """ + return False + + def is_variant_loss(self) -> bool: + """ + Checks if the current side to move lost due to a variant-specific + condition. + """ + return False + + def is_variant_win(self) -> bool: + """ + 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 variant-specific drawing condition is fulfilled. + """ + return False + + def is_game_over(self, *, claim_draw: bool = False) -> bool: + return self.outcome(claim_draw=claim_draw) is not None + + def result(self, *, claim_draw: bool = False) -> str: + outcome = self.outcome(claim_draw=claim_draw) + return outcome.result() if outcome else "*" + + def outcome(self, *, claim_draw: bool = False) -> Optional[Outcome]: + """ + Checks if the game is over due to + :func:`checkmate `, + :func:`stalemate `, + :func:`insufficient material `, + the :func:`seventyfive-move rule `, + :func:`fivefold repetition `, + or a :func:`variant end condition `. + Returns the :class:`chess.Outcome` if the game has ended, otherwise + ``None``. + + Alternatively, use :func:`~chess.Board.is_game_over()` if you are not + interested in who won the game and why. + + 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) + + # Normal game end. + if self.is_checkmate(): + return Outcome(Termination.CHECKMATE, not self.turn) + if self.is_insufficient_material(): + return Outcome(Termination.INSUFFICIENT_MATERIAL, None) + if not any(self.generate_legal_moves()): + 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) + + return None + + def is_checkmate(self) -> bool: + """Checks if the current position is a checkmate.""" + if not self.is_check(): + return False + + return not any(self.generate_legal_moves()) + + def is_stalemate(self) -> bool: + """Checks if the current position is a stalemate.""" + if self.is_check(): + return False + + if self.is_variant_end(): + return False + + return not any(self.generate_legal_moves()) + + def is_insufficient_material(self) -> bool: + """ + Checks if neither side has sufficient winning material + (:func:`~chess.Board.has_insufficient_material()`). + """ + return all(self.has_insufficient_material(color) for color in COLORS) + + def has_insufficient_material(self, color: Color) -> bool: + """ + Checks if *color* has insufficient winning material. + + This is guaranteed to return ``False`` if *color* can still win the + game. + + The converse does not necessarily hold: + The implementation only looks at the material, including the colors + of bishops, but not considering piece positions. So fortress + positions or positions with forced lines may return ``False``, even + though there is no possible winning line. + """ + if self.occupied_co[color] & (self.pawns | self.rooks | self.queens): + return False + + # Knights are only insufficient material if: + # (1) We do not have any other pieces, including more than one knight. + # (2) The opponent does not have pawns, knights, bishops or rooks. + # These would allow selfmate. + if self.occupied_co[color] & self.knights: + return (popcount(self.occupied_co[color]) <= 2 and + not (self.occupied_co[not color] & ~self.kings & ~self.queens)) + + # Bishops are only insufficient material if: + # (1) We do not have any other pieces, including bishops of the + # opposite color. + # (2) The opponent does not have bishops of the opposite color, + # pawns or knights. These would allow selfmate. + if self.occupied_co[color] & self.bishops: + same_color = (not self.bishops & BB_DARK_SQUARES) or (not self.bishops & BB_LIGHT_SQUARES) + return same_color and not self.pawns and not self.knights + + 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 + a claim by one of the players) if the half-move clock since a capture + or pawn move is equal to or greater than 150. Other means to end a game + take precedence. + """ + return self._is_halfmoves(150) + + def is_fivefold_repetition(self) -> bool: + """ + Since the 1st of July 2014 a game is automatically drawn (without + a claim by one of the players) if a position occurs for the fifth time. + Originally this had to occur on consecutive alternating moves, but + this has since been revised. + """ + return self.is_repetition(5) + + def can_claim_draw(self) -> bool: + """ + 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: + return self._is_halfmoves(100) + + def can_claim_fifty_moves(self) -> bool: + """ + Checks if the player to move can claim a draw by the fifty-move rule. + + 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, + or if there is a legal move that achieves this. Other means of ending + the game take precedence. + """ + 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 + with one of the possible legal moves. + + Note that checking this can be slow: In the worst case + scenario, every legal move has to be tested and the entire game has to + be replayed because there is no incremental transposition table. + """ + transposition_key = self._transposition_key() + transpositions: Counter[Hashable] = collections.Counter() + transpositions.update((transposition_key, )) + + # Count positions. + switchyard = [] + while self.move_stack: + move = self.pop() + switchyard.append(move) + + if self.is_irreversible(move): + break + + transpositions.update((self._transposition_key(), )) + + while switchyard: + self.push(switchyard.pop()) + + # Threefold repetition occured. + if transpositions[transposition_key] >= 3: + return True + + # The next legal move is a threefold repetition. + for move in self.generate_legal_moves(): + self.push(move) + try: + if transpositions[self._transposition_key()] >= 2: + return True + finally: + self.pop() + + return False + + def is_repetition(self, count: int = 3) -> bool: + """ + Checks if the current position has repeated 3 (or a given number of) + times. + + Unlike :func:`~chess.Board.can_claim_threefold_repetition()`, + this does not consider a repetition that can be played on the next + move. + + Note that checking this can be slow: In the worst case, the entire + game has to be replayed because there is no incremental transposition + table. + """ + # Fast check, based on occupancy only. + maybe_repetitions = 1 + for state in reversed(self._stack): + if state.occupied == self.occupied: + maybe_repetitions += 1 + if maybe_repetitions >= count: + break + if maybe_repetitions < count: + return False + + # Check full replay. + transposition_key = self._transposition_key() + switchyard = [] + + try: + while True: + if count <= 1: + return True + + if len(self.move_stack) < count - 1: + break + + move = self.pop() + switchyard.append(move) + + if self.is_irreversible(move): + break + + if self._transposition_key() == transposition_key: + count -= 1 + finally: + while switchyard: + self.push(switchyard.pop()) + + return False + + def _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: + """ + Updates the position with the given *move* and puts it onto the + move stack. + + >>> import chess + >>> + >>> board = chess.Board() + >>> + >>> Nf3 = chess.Move.from_uci("g1f3") + >>> board.push(Nf3) # Make the move + + >>> board.pop() # Unmake the last move + Move.from_uci('g1f3') + + Null moves just increment the move counters, switch turns and forfeit + en passant capturing. + + .. 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 = self._board_state() + self.castling_rights = self.clean_castling_rights() # Before pushing stack + self.move_stack.append(self._from_chess960(self.chess960, move.from_square, move.to_square, move.promotion, move.drop)) + self._stack.append(board_state) + + # Reset en passant square. + ep_square = self.ep_square + self.ep_square = None + + # Increment move counters. + self.halfmove_clock += 1 + if self.turn == BLACK: + self.fullmove_number += 1 + + # On a null move, simply swap turns and reset the en passant square. + if not move: + self.turn = not self.turn + return + + # Drops. + if move.drop: + self._set_piece_at(move.to_square, move.drop, self.turn) + self.turn = not self.turn + return + + # Zero the half-move clock. + if self.is_zeroing(move): + self.halfmove_clock = 0 + + from_bb = BB_SQUARES[move.from_square] + to_bb = BB_SQUARES[move.to_square] + + promoted = bool(self.promoted & from_bb) + piece_type = self._remove_piece_at(move.from_square) + assert piece_type is not None, f"push() expects move to be pseudo-legal, but got {move} in {self.board_fen()}" + capture_square = move.to_square + captured_piece_type = self.piece_type_at(capture_square) + + # Update castling rights. + self.castling_rights &= ~to_bb & ~from_bb + if piece_type == KING and not promoted: + 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: + self.castling_rights &= ~BB_RANK_8 + elif self.turn == BLACK and square_rank(move.to_square) == 0: + 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: + self.ep_square = move.from_square + 8 + elif diff == -16 and square_rank(move.from_square) == 6: + 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 + captured_piece_type = self._remove_piece_at(capture_square) + + # Promotion. + if move.promotion: + promoted = True + piece_type = move.promotion + + # Castling. + castling = piece_type == KING and self.occupied_co[self.turn] & to_bb + if castling: + a_side = square_file(move.to_square) < square_file(move.from_square) + + self._remove_piece_at(move.from_square) + self._remove_piece_at(move.to_square) + + if a_side: + self._set_piece_at(C1 if self.turn == WHITE else C8, KING, self.turn) + self._set_piece_at(D1 if self.turn == WHITE else D8, ROOK, self.turn) + else: + self._set_piece_at(G1 if self.turn == WHITE else G8, KING, self.turn) + self._set_piece_at(F1 if self.turn == WHITE else F8, ROOK, self.turn) + + # Put the piece on the target square. + if not castling: + was_promoted = bool(self.promoted & to_bb) + self._set_piece_at(move.to_square, piece_type, self.turn, promoted) + + if captured_piece_type: + self._push_capture(move, capture_square, captured_piece_type, was_promoted) + + # Swap turn. + self.turn = not self.turn + + def pop(self: BoardT) -> Move: + """ + Restores the previous position and returns the last move from the stack. + + :raises: :exc:`IndexError` if the move stack is empty. + """ + move = self.move_stack.pop() + self._stack.pop().restore(self) + return move + + def peek(self) -> Move: + """ + Gets the last move from the move stack. + + :raises: :exc:`IndexError` if the move stack is empty. + """ + 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:`ValueError` 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 ValueError(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 = [] + + for square in scan_reversed(castling_rights & BB_RANK_1): + builder.append(FILE_NAMES[square_file(square)].upper()) + + for square in scan_reversed(castling_rights & BB_RANK_8): + builder.append(FILE_NAMES[square_file(square)]) + + return "".join(builder) + + def castling_xfen(self) -> str: + builder = [] + + for color in COLORS: + king = self.king(color) + if king is None: + continue + + king_file = square_file(king) + backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 + + for rook_square in scan_reversed(self.clean_castling_rights() & backrank): + rook_file = square_file(rook_square) + a_side = rook_file < king_file + + other_rooks = self.occupied_co[color] & self.rooks & backrank & ~BB_SQUARES[rook_square] + + if any((square_file(other) < rook_file) == a_side for other in scan_reversed(other_rooks)): + ch = FILE_NAMES[rook_file] + else: + ch = "q" if a_side else "k" + + builder.append(ch.upper() if color == WHITE else ch) + + if builder: + return "".join(builder) + else: + return "-" + + def has_pseudo_legal_en_passant(self) -> bool: + """Checks if there is a pseudo-legal en passant capture.""" + return self.ep_square is not None and any(self.generate_pseudo_legal_ep()) + + def has_legal_en_passant(self) -> bool: + """Checks if there is a legal en passant capture.""" + return self.ep_square is not None and any(self.generate_legal_ep()) + + def fen(self, *, 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 board part :func:`~chess.Board.board_fen()`, the + :data:`~chess.Board.turn`, the castling part + (:data:`~chess.Board.castling_rights`), + the en passant square (:data:`~chess.Board.ep_square`), + the :data:`~chess.Board.halfmove_clock` + and the :data:`~chess.Board.fullmove_number`. + + :param shredder: Use :func:`~chess.Board.castling_shredder_fen()` + and encode castling rights by the file of the rook + (like ``HAha``) instead of the default + :func:`~chess.Board.castling_xfen()` (like ``KQkq``). + :param en_passant: By default, only fully legal en passant squares + are included (:func:`~chess.Board.has_legal_en_passant()`). + Pass ``fen`` to strictly follow the FEN specification + (always include the en passant square after a two-step pawn move) + or ``xfen`` to follow the X-FEN specification + (:func:`~chess.Board.has_pseudo_legal_en_passant()`). + :param promoted: Mark promoted pieces like ``Q~``. By default, this is + only enabled in chess variants where this is relevant. + """ + return " ".join([ + self.epd(shredder=shredder, en_passant=en_passant, promoted=promoted), + str(self.halfmove_clock), + str(self.fullmove_number) + ]) + + 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), + str(self.fullmove_number) + ]) + + def set_fen(self, fen: str) -> None: + """ + Parses a FEN and sets the position from it. + + :raises: :exc:`ValueError` if syntactically invalid. Use + :func:`~chess.Board.is_valid()` to detect invalid positions. + """ + parts = fen.split() + + # Board part. + try: + board_part = parts.pop(0) + except IndexError: + raise ValueError("empty fen") + + # Turn. + try: + turn_part = parts.pop(0) + except IndexError: + turn = WHITE + else: + if turn_part == "w": + turn = WHITE + elif turn_part == "b": + turn = BLACK + else: + raise ValueError(f"expected 'w' or 'b' for turn part of fen: {fen!r}") + + # Validate castling part. + try: + castling_part = parts.pop(0) + except IndexError: + castling_part = "-" + else: + if not FEN_CASTLING_REGEX.match(castling_part): + raise ValueError(f"invalid castling part in fen: {fen!r}") + + # En passant square. + try: + ep_part = parts.pop(0) + except IndexError: + ep_square = None + else: + try: + ep_square = None if ep_part == "-" else SQUARE_NAMES.index(ep_part) + except ValueError: + raise ValueError(f"invalid en passant part in fen: {fen!r}") + + # Check that the half-move part is valid. + try: + halfmove_part = parts.pop(0) + except IndexError: + halfmove_clock = 0 + else: + try: + halfmove_clock = int(halfmove_part) + except ValueError: + raise ValueError(f"invalid half-move clock in fen: {fen!r}") + + if halfmove_clock < 0: + raise ValueError(f"half-move clock cannot be negative: {fen!r}") + + # Check that the full-move number part is valid. + # 0 is allowed for compatibility, but later replaced with 1. + try: + fullmove_part = parts.pop(0) + except IndexError: + fullmove_number = 1 + else: + try: + fullmove_number = int(fullmove_part) + except ValueError: + raise ValueError(f"invalid fullmove number in fen: {fen!r}") + + if fullmove_number < 0: + raise ValueError(f"fullmove number cannot be negative: {fen!r}") + + fullmove_number = max(fullmove_number, 1) + + # All parts should be consumed now. + if parts: + raise ValueError(f"fen string has more parts than expected: {fen!r}") + + # Validate the board part and set it. + self._set_board_fen(board_part) + + # Apply. + self.turn = turn + self._set_castling_fen(castling_part) + self.ep_square = ep_square + self.halfmove_clock = halfmove_clock + self.fullmove_number = fullmove_number + self.clear_stack() + + def _set_castling_fen(self, castling_fen: str) -> None: + if not castling_fen or castling_fen == "-": + self.castling_rights = BB_EMPTY + return + + if not FEN_CASTLING_REGEX.match(castling_fen): + raise ValueError(f"invalid castling fen: {castling_fen!r}") + + self.castling_rights = BB_EMPTY + + for flag in castling_fen: + color = WHITE if flag.isupper() else BLACK + flag = flag.lower() + backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 + rooks = self.occupied_co[color] & self.rooks & backrank + king = self.king(color) + + if flag == "q": + # Select the leftmost rook. + if king is not None and lsb(rooks) < king: + self.castling_rights |= rooks & -rooks + else: + self.castling_rights |= BB_FILE_A & backrank + elif flag == "k": + # Select the rightmost rook. + rook = msb(rooks) + if king is not None and king < rook: + self.castling_rights |= BB_SQUARES[rook] + else: + self.castling_rights |= BB_FILE_H & backrank + else: + self.castling_rights |= BB_FILES[FILE_NAMES.index(flag)] & backrank + + def set_castling_fen(self, castling_fen: str) -> None: + """ + Sets castling rights from a string in FEN notation like ``Qqk``. + + :raises: :exc:`ValueError` if the castling FEN is syntactically + invalid. + """ + self._set_castling_fen(castling_fen) + self.clear_stack() + + def set_board_fen(self, fen: str) -> None: + super().set_board_fen(fen) + self.clear_stack() + + def set_piece_map(self, pieces: Mapping[Square, Piece]) -> None: + super().set_piece_map(pieces) + self.clear_stack() + + def set_chess960_pos(self, scharnagl: int) -> None: + super().set_chess960_pos(scharnagl) + self.chess960 = True + self.turn = WHITE + self.castling_rights = self.rooks + self.ep_square = None + self.halfmove_clock = 0 + self.fullmove_number = 1 + + self.clear_stack() + + 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, + or ``None`` if the current position is not a Chess960 starting + position. + + By default, white to move (**ignore_turn**) and full castling rights + (**ignore_castling**) are required, but move counters + (**ignore_counters**) are ignored. + """ + if self.ep_square: + return None + + if not ignore_turn: + if self.turn != WHITE: + return None + + if not ignore_castling: + if self.clean_castling_rights() != self.rooks: + return None + + if not ignore_counters: + if self.fullmove_number != 1 or self.halfmove_clock != 0: + return None + + return super().chess960_pos() + + def _epd_operations(self, operations: Mapping[str, Union[None, str, int, float, Move, Iterable[Move]]]) -> str: + epd = [] + 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}" + + if not first_op: + epd.append(" ") + first_op = False + epd.append(opcode) + + if operand is None: + epd.append(";") + elif isinstance(operand, Move): + epd.append(" ") + epd.append(self.san(operand)) + epd.append(";") + elif isinstance(operand, int): + epd.append(f" {operand};") + elif isinstance(operand, float): + assert math.isfinite(operand), f"expected numeric epd operand to be finite, got: {operand}" + epd.append(f" {operand};") + elif opcode == "pv" and not isinstance(operand, str) and hasattr(operand, "__iter__"): + position = self.copy(stack=False) + for move in operand: + epd.append(" ") + epd.append(position.san_and_push(move)) + epd.append(";") + elif opcode in ["am", "bm"] and not isinstance(operand, str) and hasattr(operand, "__iter__"): + for san in sorted(self.san(move) for move in operand): + epd.append(" ") + epd.append(san) + epd.append(";") + else: + # Append as escaped string. + epd.append(" \"") + epd.append(str(operand).replace("\\", "\\\\").replace("\t", "\\t").replace("\r", "\\r").replace("\n", "\\n").replace("\"", "\\\"")) + epd.append("\";") + + return "".join(epd) + + def epd(self, *, shredder: bool = False, en_passant: _EnPassantSpec = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, Move, Iterable[Move]]) -> str: + """ + Gets an EPD representation of the current position. + + See :func:`~chess.Board.fen()` for FEN formatting options (*shredder*, + *ep_square* and *promoted*). + + EPD operations can be given as keyword arguments. Supported operands + are strings, integers, finite floats, legal moves and ``None``. + Additionally, the operation ``pv`` accepts a legal variation as + a list of moves. The operations ``am`` and ``bm`` accept a list of + legal moves in the current position. + + The name of the field cannot be a lone dash and cannot contain spaces, + newlines, carriage returns or tabs. + + *hmvc* and *fmvn* are not included by default. You can use: + + >>> import chess + >>> + >>> board = chess.Board() + >>> board.epd(hmvc=board.halfmove_clock, fmvn=board.fullmove_number) + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - hmvc 0; fmvn 1;' + """ + if en_passant == "fen": + ep_square = self.ep_square + elif en_passant == "xfen": + ep_square = self.ep_square if self.has_pseudo_legal_en_passant() else None + else: + ep_square = self.ep_square if self.has_legal_en_passant() else None + + epd = [self.board_fen(promoted=promoted), + "w" if self.turn == WHITE else "b", + self.castling_shredder_fen() if shredder else self.castling_xfen(), + SQUARE_NAMES[ep_square] if ep_square is not None else "-"] + + if operations: + epd.append(self._epd_operations(operations)) + + return " ".join(epd) + + def _parse_epd_ops(self: BoardT, operation_part: str, make_board: Callable[[], BoardT]) -> Dict[str, Union[None, str, int, float, Move, List[Move]]]: + operations: Dict[str, Union[None, str, int, float, Move, List[Move]]] = {} + state = "opcode" + opcode = "" + operand = "" + position = None + + for ch in itertools.chain(operation_part, [None]): + if state == "opcode": + if ch in [" ", "\t", "\r", "\n"]: + if opcode == "-": + opcode = "" + elif opcode: + state = "after_opcode" + elif ch is None or ch == ";": + if opcode == "-": + opcode = "" + elif opcode: + operations[opcode] = [] if opcode in ["pv", "am", "bm"] else None + opcode = "" + else: + opcode += ch + elif state == "after_opcode": + if ch in [" ", "\t", "\r", "\n"]: + pass + elif ch == "\"": + state = "string" + elif ch is None or ch == ";": + if opcode: + operations[opcode] = [] if opcode in ["pv", "am", "bm"] else None + opcode = "" + state = "opcode" + elif ch in "+-.0123456789": + operand = ch + state = "numeric" + else: + operand = ch + state = "san" + elif state == "numeric": + if ch is None or ch == ";": + if "." in operand or "e" in operand or "E" in operand: + parsed = float(operand) + if not math.isfinite(parsed): + raise ValueError(f"invalid numeric operand for epd operation {opcode!r}: {operand!r}") + operations[opcode] = parsed + else: + operations[opcode] = int(operand) + opcode = "" + operand = "" + state = "opcode" + else: + operand += ch + elif state == "string": + if ch is None or ch == "\"": + operations[opcode] = operand + opcode = "" + operand = "" + state = "opcode" + elif ch == "\\": + state = "string_escape" + else: + operand += ch + elif state == "string_escape": + if ch is None: + operations[opcode] = operand + opcode = "" + operand = "" + state = "opcode" + elif ch == "r": + operand += "\r" + state = "string" + elif ch == "n": + operand += "\n" + state = "string" + elif ch == "t": + operand += "\t" + state = "string" + else: + operand += ch + state = "string" + elif state == "san": + if ch is None or ch == ";": + if position is None: + position = make_board() + + if opcode == "pv": + # A variation. + variation = [] + for token in operand.split(): + move = position.parse_xboard(token) + variation.append(move) + position.push(move) + + # Reset the position. + while position.move_stack: + position.pop() + + operations[opcode] = variation + elif opcode in ["bm", "am"]: + # A set of moves. + operations[opcode] = [position.parse_xboard(token) for token in operand.split()] + else: + # A single move. + operations[opcode] = position.parse_xboard(operand) + + opcode = "" + operand = "" + state = "opcode" + else: + operand += ch + + assert state == "opcode" + return operations + + def set_epd(self, epd: str) -> Dict[str, Union[None, str, int, float, Move, List[Move]]]: + """ + Parses the given EPD string and uses it to set the position. + + If present, ``hmvc`` and ``fmvn`` are used to set the half-move + clock and the full-move number. Otherwise, ``0`` and ``1`` are used. + + Returns a dictionary of parsed operations. Values can be strings, + integers, floats, move objects, or lists of moves. + + :raises: :exc:`ValueError` if the EPD string is invalid. + """ + parts = epd.strip().rstrip(";").split(None, 4) + + # Parse ops. + if len(parts) > 4: + operations = self._parse_epd_ops(parts.pop(), lambda: type(self)(" ".join(parts) + " 0 1")) + parts.append(str(operations["hmvc"]) if "hmvc" in operations else "0") + parts.append(str(operations["fmvn"]) if "fmvn" in operations else "1") + self.set_fen(" ".join(parts)) + return operations + else: + self.set_fen(epd) + return {} + + def san(self, move: Move) -> str: + """ + Gets the standard algebraic notation of the given move in the context + of the current position. + """ + return self._algebraic(move) + + def lan(self, move: Move) -> str: + """ + Gets the long algebraic notation of the given move in the context of + the current position. + """ + return self._algebraic(move, long=True) + + def san_and_push(self, move: Move) -> str: + return self._algebraic_and_push(move) + + def _algebraic(self, move: Move, *, long: bool = False) -> str: + san = self._algebraic_and_push(move, long=long) + self.pop() + return san + + def _algebraic_and_push(self, move: Move, *, long: bool = False) -> str: + san = self._algebraic_without_suffix(move, long=long) + + # Look ahead for check or checkmate. + self.push(move) + is_check = self.is_check() + is_checkmate = (is_check and self.is_checkmate()) or self.is_variant_loss() or self.is_variant_win() + + # Add check or checkmate suffix. + if is_checkmate and move: + return san + "#" + elif is_check and move: + return san + "+" + else: + return san + + def _algebraic_without_suffix(self, move: Move, *, long: bool = False) -> str: + # Null move. + if not move: + return "--" + + # Drops. + if move.drop: + san = "" + if move.drop != PAWN: + san = piece_symbol(move.drop).upper() + san += "@" + SQUARE_NAMES[move.to_square] + return san + + # Castling. + if self.is_castling(move): + if square_file(move.to_square) < square_file(move.from_square): + return "O-O-O" + else: + return "O-O" + + piece_type = self.piece_type_at(move.from_square) + assert piece_type, f"san() and lan() expect move to be legal or null, but got {move} in {self.fen()}" + capture = self.is_capture(move) + + if piece_type == PAWN: + san = "" + else: + san = piece_symbol(piece_type).upper() + + if long: + san += SQUARE_NAMES[move.from_square] + elif piece_type != PAWN: + # Get ambiguous move candidates. + # Relevant candidates: not exactly the current move, + # but to the same square. + others = 0 + from_mask = self.pieces_mask(piece_type, self.turn) + from_mask &= ~BB_SQUARES[move.from_square] + to_mask = BB_SQUARES[move.to_square] + for candidate in self.generate_legal_moves(from_mask, to_mask): + others |= BB_SQUARES[candidate.from_square] + + # Disambiguate. + if others: + row, column = False, False + + if others & BB_RANKS[square_rank(move.from_square)]: + column = True + + if others & BB_FILES[square_file(move.from_square)]: + row = True + else: + column = True + + if column: + san += FILE_NAMES[square_file(move.from_square)] + if row: + san += RANK_NAMES[square_rank(move.from_square)] + elif capture: + san += FILE_NAMES[square_file(move.from_square)] + + # Captures. + if capture: + san += "x" + elif long: + san += "-" + + # Destination square. + san += SQUARE_NAMES[move.to_square] + + # Promotion. + if move.promotion: + san += "=" + piece_symbol(move.promotion).upper() + + return san + + def variation_san(self, variation: Iterable[Move]) -> str: + """ + Given a sequence of moves, returns a string representing the sequence + in standard algebraic notation (e.g., ``1. e4 e5 2. Nf3 Nc6`` or + ``37...Bg6 38. fxg6``). + + The board will not be modified as a result of calling this. + + :raises: :exc:`ValueError` if any moves in the sequence are illegal. + """ + board = self.copy(stack=False) + san = [] + + for move in variation: + if not board.is_legal(move): + raise ValueError(f"illegal move {move} in position {board.fen()}") + + if board.turn == WHITE: + san.append(f"{board.fullmove_number}. {board.san_and_push(move)}") + elif not san: + san.append(f"{board.fullmove_number}...{board.san_and_push(move)}") + else: + san.append(board.san_and_push(move)) + + return " ".join(san) + + 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. + + The returned move is guaranteed to be either legal or a null move. + + :raises: :exc:`ValueError` if the SAN is invalid, illegal or ambiguous. + """ + # Castling. + try: + 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#", "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()}") + + # Match normal moves. + match = SAN_REGEX.match(san) + if not match: + # Null moves. + if san in ["--", "Z0", "0000", "@@@@"]: + return Move.null() + elif "," in san: + raise ValueError(f"unsupported multi-leg move: {san!r}") + else: + raise ValueError(f"invalid san: {san!r}") + + # 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 piece type. + p = match.group(5) + promotion = PIECE_SYMBOLS.index(p[-1].lower()) if p else None + + # Filter by original square. + from_mask = BB_ALL + 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) + elif match.group(2) and match.group(3): + # 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 ValueError(f"missing promotion piece type: {san!r} in {self.fen()}") + else: + from_mask &= self.pawns + + # Match legal moves. + matched_move = None + for move in self.generate_legal_moves(from_mask, to_mask): + if move.promotion != promotion: + continue + + if matched_move: + raise ValueError(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()}") + + return matched_move + + def push_san(self, san: str) -> Move: + """ + Parses a move in standard algebraic notation, makes the move and puts + it onto the move stack. + + Returns the move. + + :raises: :exc:`ValueError` if neither legal nor a null move. + """ + move = self.parse_san(san) + self.push(move) + return move + + def uci(self, move: Move, *, chess960: Optional[bool] = None) -> str: + """ + Gets the UCI notation of the move. + + *chess960* defaults to the mode of the board. Pass ``True`` to force + Chess960 mode. + """ + if chess960 is None: + chess960 = self.chess960 + + move = self._to_chess960(move) + move = self._from_chess960(chess960, move.from_square, move.to_square, move.promotion, move.drop) + return move.uci() + + def parse_uci(self, uci: str) -> Move: + """ + Parses the given move in UCI notation. + + Supports both Chess960 and standard UCI notation. + + 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 + current position (but not a null move). + """ + move = Move.from_uci(uci) + + if not move: + return move + + 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 uci: {uci!r} in {self.fen()}") + + return move + + def push_uci(self, uci: str) -> Move: + """ + Parses a move in UCI notation and puts it on the move stack. + + Returns the move. + + :raises: :exc:`ValueError` if the move is invalid or illegal in the + current position (but not a null move). + """ + move = self.parse_uci(uci) + self.push(move) + return move + + def xboard(self, move: Move, chess960: Optional[bool] = None) -> str: + if chess960 is None: + chess960 = self.chess960 + + if not chess960 or not self.is_castling(move): + return move.xboard() + elif self.is_kingside_castling(move): + return "O-O" + else: + return "O-O-O" + + def parse_xboard(self, xboard: str) -> Move: + return self.parse_san(xboard) + + push_xboard = push_san + + def is_en_passant(self, move: Move) -> bool: + """Checks if the given pseudo-legal move is an en passant capture.""" + return (self.ep_square == move.to_square and + bool(self.pawns & BB_SQUARES[move.from_square]) and + abs(move.to_square - move.from_square) in [7, 9] and + not self.occupied & BB_SQUARES[move.to_square]) + + def is_capture(self, move: Move) -> bool: + """Checks if the given pseudo-legal move is a capture.""" + touched = BB_SQUARES[move.from_square] ^ BB_SQUARES[move.to_square] + return bool(touched & self.occupied_co[not self.turn]) or self.is_en_passant(move) + + def is_zeroing(self, move: Move) -> bool: + """Checks if the given pseudo-legal move is a capture or pawn move.""" + touched = BB_SQUARES[move.from_square] ^ BB_SQUARES[move.to_square] + return bool(touched & self.pawns or touched & self.occupied_co[not self.turn] or move.drop == PAWN) + + def _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) + + def is_irreversible(self, move: Move) -> bool: + """ + Checks if the given pseudo-legal move is 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) or self.has_legal_en_passant() + + def is_castling(self, move: Move) -> bool: + """Checks if the given pseudo-legal move is a castling move.""" + if self.kings & BB_SQUARES[move.from_square]: + diff = square_file(move.from_square) - square_file(move.to_square) + return abs(diff) > 1 or bool(self.rooks & self.occupied_co[self.turn] & BB_SQUARES[move.to_square]) + return False + + def is_kingside_castling(self, move: Move) -> bool: + """ + Checks if the given pseudo-legal move is a kingside castling move. + """ + return self.is_castling(move) and square_file(move.to_square) > square_file(move.from_square) + + def is_queenside_castling(self, move: Move) -> bool: + """ + Checks if the given pseudo-legal move is a queenside castling move. + """ + return self.is_castling(move) and square_file(move.to_square) < square_file(move.from_square) + + def clean_castling_rights(self) -> Bitboard: + """ + Returns valid castling rights filtered from + :data:`~chess.Board.castling_rights`. + """ + if self._stack: + # No new castling rights are assigned in a game, so we can assume + # they were filtered already. + return self.castling_rights + + castling = self.castling_rights & self.rooks + white_castling = castling & BB_RANK_1 & self.occupied_co[WHITE] + black_castling = castling & BB_RANK_8 & self.occupied_co[BLACK] + + if not self.chess960: + # The rooks must be on a1, h1, a8 or h8. + white_castling &= (BB_A1 | BB_H1) + 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: + white_castling = 0 + if not self.occupied_co[BLACK] & self.kings & ~self.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 + if not white_king_mask: + white_castling = 0 + if not black_king_mask: + black_castling = 0 + + # There are only two ways of castling, a-side and h-side, and the + # king must be between the rooks. + white_a_side = white_castling & -white_castling + white_h_side = BB_SQUARES[msb(white_castling)] if white_castling else 0 + + if white_a_side and msb(white_a_side) > msb(white_king_mask): + white_a_side = 0 + if white_h_side and msb(white_h_side) < msb(white_king_mask): + white_h_side = 0 + + black_a_side = (black_castling & -black_castling) + black_h_side = BB_SQUARES[msb(black_castling)] if black_castling else BB_EMPTY + + if black_a_side and msb(black_a_side) > msb(black_king_mask): + black_a_side = 0 + if black_h_side and msb(black_h_side) < msb(black_king_mask): + black_h_side = 0 + + # Done. + return black_a_side | black_h_side | white_a_side | white_h_side + + def has_castling_rights(self, color: Color) -> bool: + """Checks if the given side has castling rights.""" + backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 + return bool(self.clean_castling_rights() & backrank) + + def has_kingside_castling_rights(self, color: Color) -> bool: + """ + Checks if the given side has kingside (that is h-side in Chess960) + castling rights. + """ + backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 + king_mask = self.kings & self.occupied_co[color] & backrank & ~self.promoted + if not king_mask: + return False + + castling_rights = self.clean_castling_rights() & backrank + while castling_rights: + rook = castling_rights & -castling_rights + + if rook > king_mask: + return True + + castling_rights &= castling_rights - 1 + + return False + + def has_queenside_castling_rights(self, color: Color) -> bool: + """ + Checks if the given side has queenside (that is a-side in Chess960) + castling rights. + """ + backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 + king_mask = self.kings & self.occupied_co[color] & backrank & ~self.promoted + if not king_mask: + return False + + castling_rights = self.clean_castling_rights() & backrank + while castling_rights: + rook = castling_rights & -castling_rights + + if rook < king_mask: + return True + + castling_rights &= castling_rights - 1 + + return False + + def has_chess960_castling_rights(self) -> bool: + """ + Checks if there are castling rights that are only possible in Chess960. + """ + # Get valid Chess960 castling rights. + chess960 = self.chess960 + self.chess960 = True + castling_rights = self.clean_castling_rights() + self.chess960 = chess960 + + # Standard chess castling rights can only be on the standard + # starting rook squares. + if castling_rights & ~BB_CORNERS: + return True + + # If there are any castling rights in standard chess, the king must be + # on e1 or e8. + if castling_rights & BB_RANK_1 and not self.occupied_co[WHITE] & self.kings & BB_E1: + return True + if castling_rights & BB_RANK_8 and not self.occupied_co[BLACK] & self.kings & BB_E8: + return True + + return False + + def status(self) -> Status: + """ + Gets a bitmask of possible problems with the position. + + :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. + + Otherwise, bitwise combinations of: + :data:`~chess.STATUS_NO_WHITE_KING`, + :data:`~chess.STATUS_NO_BLACK_KING`, + :data:`~chess.STATUS_TOO_MANY_KINGS`, + :data:`~chess.STATUS_TOO_MANY_WHITE_PAWNS`, + :data:`~chess.STATUS_TOO_MANY_BLACK_PAWNS`, + :data:`~chess.STATUS_PAWNS_ON_BACKRANK`, + :data:`~chess.STATUS_TOO_MANY_WHITE_PIECES`, + :data:`~chess.STATUS_TOO_MANY_BLACK_PIECES`, + :data:`~chess.STATUS_BAD_CASTLING_RIGHTS`, + :data:`~chess.STATUS_INVALID_EP_SQUARE`, + :data:`~chess.STATUS_OPPOSITE_CHECK`, + :data:`~chess.STATUS_EMPTY`, + :data:`~chess.STATUS_RACE_CHECK`, + :data:`~chess.STATUS_RACE_OVER`, + :data:`~chess.STATUS_RACE_MATERIAL`, + :data:`~chess.STATUS_TOO_MANY_CHECKERS`, + :data:`~chess.STATUS_IMPOSSIBLE_CHECK`. + """ + errors = STATUS_VALID + + # There must be at least one piece. + if not self.occupied: + errors |= STATUS_EMPTY + + # There must be exactly one king of each color. + if not self.occupied_co[WHITE] & self.kings: + errors |= STATUS_NO_WHITE_KING + if not self.occupied_co[BLACK] & self.kings: + errors |= STATUS_NO_BLACK_KING + if popcount(self.occupied & self.kings) > 2: + errors |= STATUS_TOO_MANY_KINGS + + # There can not be more than 16 pieces of any color. + if popcount(self.occupied_co[WHITE]) > 16: + errors |= STATUS_TOO_MANY_WHITE_PIECES + if popcount(self.occupied_co[BLACK]) > 16: + errors |= STATUS_TOO_MANY_BLACK_PIECES + + # There can not be more than 8 pawns of any color. + if popcount(self.occupied_co[WHITE] & self.pawns) > 8: + errors |= STATUS_TOO_MANY_WHITE_PAWNS + if popcount(self.occupied_co[BLACK] & self.pawns) > 8: + errors |= STATUS_TOO_MANY_BLACK_PAWNS + + # Pawns can not be on the back rank. + if self.pawns & BB_BACKRANKS: + errors |= STATUS_PAWNS_ON_BACKRANK + + # Castling rights. + if self.castling_rights != self.clean_castling_rights(): + errors |= STATUS_BAD_CASTLING_RIGHTS + + # En passant. + valid_ep_square = self._valid_ep_square() + if self.ep_square != valid_ep_square: + errors |= STATUS_INVALID_EP_SQUARE + + # Side to move giving check. + if self.was_into_check(): + errors |= STATUS_OPPOSITE_CHECK + + # More than the maximum number of possible checkers in the variant. + checkers = self.checkers_mask() + our_kings = self.kings & self.occupied_co[self.turn] & ~self.promoted + if popcount(checkers) > 2: + errors |= STATUS_TOO_MANY_CHECKERS + elif popcount(checkers) == 2 and ray(lsb(checkers), msb(checkers)) & our_kings: + errors |= STATUS_IMPOSSIBLE_CHECK + elif valid_ep_square is not None and any(ray(checker, valid_ep_square) & our_kings for checker in scan_reversed(checkers)): + errors |= STATUS_IMPOSSIBLE_CHECK + + return errors + + def _valid_ep_square(self) -> Optional[Square]: + if not self.ep_square: + return None + + if self.turn == WHITE: + ep_rank = 5 + pawn_mask = shift_down(BB_SQUARES[self.ep_square]) + seventh_rank_mask = shift_up(BB_SQUARES[self.ep_square]) + else: + ep_rank = 2 + pawn_mask = shift_up(BB_SQUARES[self.ep_square]) + seventh_rank_mask = shift_down(BB_SQUARES[self.ep_square]) + + # The en passant square must be on the third or sixth rank. + if square_rank(self.ep_square) != ep_rank: + return None + + # The last move must have been a double pawn push, so there must + # be a pawn of the correct color on the fourth or fifth rank. + if not self.pawns & self.occupied_co[not self.turn] & pawn_mask: + return None + + # And the en passant square must be empty. + if self.occupied & BB_SQUARES[self.ep_square]: + return None + + # And the second rank must be empty. + if self.occupied & seventh_rank_mask: + return None + + return self.ep_square + + def is_valid(self) -> bool: + """ + Checks some basic validity requirements. + + See :func:`~chess.Board.status()` for details. + """ + return self.status() == STATUS_VALID + + def _ep_skewered(self, king: Square, capturer: Square) -> bool: + # Handle the special case where the king would be in check if the + # pawn and its capturer disappear from the rank. + + # Vertical skewers of the captured pawn are not possible. (Pins on + # the capturer are not handled here.) + assert self.ep_square is not None + + last_double = self.ep_square + (-8 if self.turn == WHITE else 8) + + occupancy = (self.occupied & ~BB_SQUARES[last_double] & + ~BB_SQUARES[capturer] | BB_SQUARES[self.ep_square]) + + # Horizontal attack on the fifth or fourth rank. + horizontal_attackers = self.occupied_co[not self.turn] & (self.rooks | self.queens) + if BB_RANK_ATTACKS[king][BB_RANK_MASKS[king] & occupancy] & horizontal_attackers: + return True + + # Diagonal skewers. These are not actually possible in a real game, + # because if the latest double pawn move covers a diagonal attack, + # then the other side would have been in check already. + diagonal_attackers = self.occupied_co[not self.turn] & (self.bishops | self.queens) + if BB_DIAG_ATTACKS[king][BB_DIAG_MASKS[king] & occupancy] & diagonal_attackers: + return True + + return False + + def _slider_blockers(self, king: Square) -> Bitboard: + rooks_and_queens = self.rooks | self.queens + bishops_and_queens = self.bishops | self.queens + + snipers = ((BB_RANK_ATTACKS[king][0] & rooks_and_queens) | + (BB_FILE_ATTACKS[king][0] & rooks_and_queens) | + (BB_DIAG_ATTACKS[king][0] & bishops_and_queens)) + + blockers = 0 + + for sniper in scan_reversed(snipers & self.occupied_co[not self.turn]): + b = between(king, sniper) & self.occupied + + # Add to blockers if exactly one piece in-between. + if b and BB_SQUARES[msb(b)] == b: + blockers |= b + + return blockers & self.occupied_co[self.turn] + + def _is_safe(self, king: Square, blockers: Bitboard, move: Move) -> bool: + if move.from_square == king: + if self.is_castling(move): + return True + else: + return not self.is_attacked_by(not self.turn, move.to_square) + elif self.is_en_passant(move): + return bool(self.pin_mask(self.turn, move.from_square) & BB_SQUARES[move.to_square] and + not self._ep_skewered(king, move.from_square)) + else: + return bool(not blockers & BB_SQUARES[move.from_square] or + 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 |= 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): + yield Move(king, to_square) + + checker = msb(checkers) + if BB_SQUARES[checker] == checkers: + # Capture or block a single checker. + target = between(king, checker) | checkers + + yield from self.generate_pseudo_legal_moves(~self.kings & from_mask, target & to_mask) + + # Capture the checking pawn en passant (but avoid yielding + # duplicate moves). + if self.ep_square and not BB_SQUARES[self.ep_square] & target: + last_double = self.ep_square + (-8 if self.turn == WHITE else 8) + if last_double == checker: + yield from self.generate_pseudo_legal_ep(from_mask, to_mask) + + def generate_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: + if self.is_variant_end(): + return + + king_mask = self.kings & self.occupied_co[self.turn] + if king_mask: + king = msb(king_mask) + blockers = self._slider_blockers(king) + checkers = self.attackers_mask(not self.turn, king) + if checkers: + for move in self._generate_evasions(king, checkers, from_mask, to_mask): + if self._is_safe(king, blockers, move): + yield move + else: + for move in self.generate_pseudo_legal_moves(from_mask, to_mask): + if self._is_safe(king, blockers, move): + yield move + else: + yield from self.generate_pseudo_legal_moves(from_mask, to_mask) + + def generate_legal_ep(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: + if self.is_variant_end(): + return + + for move in self.generate_pseudo_legal_ep(from_mask, to_mask): + if not self.is_into_check(move): + yield move + + def generate_legal_captures(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: + return itertools.chain( + self.generate_legal_moves(from_mask, to_mask & self.occupied_co[not self.turn]), + self.generate_legal_ep(from_mask, to_mask)) + + def _attacked_for_king(self, path: Bitboard, occupied: Bitboard) -> bool: + return any(self._attackers_mask(not self.turn, sq, occupied) for sq in scan_reversed(path)) + + def generate_castling_moves(self, from_mask: 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 + if not king: + return + + bb_c = BB_FILE_C & backrank + bb_d = BB_FILE_D & backrank + bb_f = BB_FILE_F & backrank + bb_g = BB_FILE_G & backrank + + for candidate in scan_reversed(self.clean_castling_rights() & backrank & to_mask): + rook = BB_SQUARES[candidate] + + a_side = rook < king + king_to = bb_c if a_side else bb_g + rook_to = bb_d if a_side else bb_f + + king_path = between(msb(king), msb(king_to)) + rook_path = between(candidate, msb(rook_to)) + + if not ((self.occupied ^ king ^ rook) & (king_path | rook_path | king_to | rook_to) or + self._attacked_for_king(king_path | king, self.occupied ^ king) or + self._attacked_for_king(king_to, self.occupied ^ king ^ rook ^ rook_to)): + yield self._from_chess960(self.chess960, msb(king), candidate) + + def _from_chess960(self, chess960: bool, from_square: Square, to_square: Square, promotion: Optional[PieceType] = None, drop: Optional[PieceType] = None) -> Move: + if not chess960 and promotion is None and drop is None: + if from_square == E1 and self.kings & BB_E1: + if to_square == H1: + return Move(E1, G1) + elif to_square == A1: + return Move(E1, C1) + elif from_square == E8 and self.kings & BB_E8: + if to_square == H8: + return Move(E8, G8) + elif to_square == A8: + return Move(E8, C8) + + return Move(from_square, to_square, promotion, drop) + + def _to_chess960(self, move: Move) -> Move: + if move.from_square == E1 and self.kings & BB_E1: + if move.to_square == G1 and not self.rooks & BB_G1: + return Move(E1, H1) + elif move.to_square == C1 and not self.rooks & BB_C1: + return Move(E1, A1) + elif move.from_square == E8 and self.kings & BB_E8: + if move.to_square == G8 and not self.rooks & BB_G8: + return Move(E8, H8) + elif move.to_square == C8 and not self.rooks & BB_C8: + return Move(E8, A8) + + return move + + def _transposition_key(self) -> Hashable: + return (self.pawns, self.knights, self.bishops, self.rooks, + self.queens, self.kings, + self.occupied_co[WHITE], self.occupied_co[BLACK], + self.turn, self.clean_castling_rights(), + self.ep_square if self.has_legal_en_passant() else None) + + def __repr__(self) -> str: + if not self.chess960: + return f"{type(self).__name__}({self.fen()!r})" + else: + return f"{type(self).__name__}({self.fen()!r}, chess960=True)" + + def _repr_svg_(self) -> str: + import chess.svg + return chess.svg.board( + board=self, + size=390, + lastmove=self.peek() if self.move_stack else None, + check=self.king(self.turn) if self.is_check() else None) + + def __eq__(self, board: object) -> bool: + if isinstance(board, Board): + return ( + self.halfmove_clock == board.halfmove_clock and + self.fullmove_number == board.fullmove_number and + type(self).uci_variant == type(board).uci_variant and + self._transposition_key() == board._transposition_key()) + else: + return NotImplemented + + def 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: + board = self.copy(stack=False) + board.apply_transform(f) + return board + + def apply_mirror(self: BoardT) -> None: + super().apply_mirror() + self.turn = not self.turn + + def mirror(self: BoardT) -> BoardT: + """ + 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: + """ + Creates a copy of the board. + + Defaults to copying the entire move stack. Alternatively, *stack* can + be ``False``, or an integer to copy a limited number of moves. + """ + board = super().copy() + + board.chess960 = self.chess960 + + board.ep_square = self.ep_square + board.castling_rights = self.castling_rights + board.turn = self.turn + board.fullmove_number = self.fullmove_number + board.halfmove_clock = self.halfmove_clock + + if stack: + stack = len(self.move_stack) if stack is True else stack + board.move_stack = [copy.copy(move) for move in self.move_stack[-stack:]] + board._stack = self._stack[-stack:] + + return board + + @classmethod + def empty(cls: Type[BoardT], *, chess960: bool = False) -> BoardT: + """Creates a new empty board. Also see :func:`~chess.Board.clear()`.""" + return cls(None, chess960=chess960) + + @classmethod + def from_epd(cls: Type[BoardT], epd: str, *, chess960: bool = False) -> Tuple[BoardT, Dict[str, Union[None, str, int, float, Move, List[Move]]]]: + """ + Creates a new board from an EPD string. See + :func:`~chess.Board.set_epd()`. + + Returns the board and the dictionary of parsed operations as a tuple. + """ + board = cls.empty(chess960=chess960) + return board, board.set_epd(epd) + + @classmethod + def from_chess960_pos(cls: Type[BoardT], scharnagl: int) -> BoardT: + board = cls.empty(chess960=True) + board.set_chess960_pos(scharnagl) + return board + + +class PseudoLegalMoveGenerator: + + def __init__(self, board: Board) -> None: + self.board = board + + def __bool__(self) -> bool: + return any(self.board.generate_pseudo_legal_moves()) + + def count(self) -> int: + # List conversion is faster than iterating. + return len(list(self)) + + def __iter__(self) -> Iterator[Move]: + return self.board.generate_pseudo_legal_moves() + + def __contains__(self, move: Move) -> bool: + return self.board.is_pseudo_legal(move) + + def __repr__(self) -> str: + builder = [] + + for move in self: + if self.board.is_legal(move): + builder.append(self.board.san(move)) + else: + builder.append(self.board.uci(move)) + + sans = ", ".join(builder) + return f"" + + +class LegalMoveGenerator: + + def __init__(self, board: Board) -> None: + self.board = board + + def __bool__(self) -> bool: + return any(self.board.generate_legal_moves()) + + def count(self) -> int: + # List conversion is faster than iterating. + return len(list(self)) + + def __iter__(self) -> Iterator[Move]: + return self.board.generate_legal_moves() + + def __contains__(self, move: Move) -> bool: + return self.board.is_legal(move) + + def __repr__(self) -> str: + sans = ", ".join(self.board.san(move) for move in self) + return f"" + + +IntoSquareSet = Union[SupportsInt, Iterable[Square]] + +class SquareSet: + """ + A set of squares. + + >>> import chess + >>> + >>> squares = chess.SquareSet([chess.A8, chess.A1]) + >>> squares + SquareSet(0x0100_0000_0000_0001) + + >>> squares = chess.SquareSet(chess.BB_A8 | chess.BB_RANK_1) + >>> squares + SquareSet(0x0100_0000_0000_00ff) + + >>> print(squares) + 1 . . . . . . . + . . . . . . . . + . . . . . . . . + . . . . . . . . + . . . . . . . . + . . . . . . . . + . . . . . . . . + 1 1 1 1 1 1 1 1 + + >>> len(squares) + 9 + + >>> bool(squares) + True + + >>> chess.B1 in squares + True + + >>> for square in squares: + ... # 0 -- chess.A1 + ... # 1 -- chess.B1 + ... # 2 -- chess.C1 + ... # 3 -- chess.D1 + ... # 4 -- chess.E1 + ... # 5 -- chess.F1 + ... # 6 -- chess.G1 + ... # 7 -- chess.H1 + ... # 56 -- chess.A8 + ... print(square) + ... + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 56 + + >>> list(squares) + [0, 1, 2, 3, 4, 5, 6, 7, 56] + + Square sets are internally represented by 64-bit integer masks of the + included squares. Bitwise operations can be used to compute unions, + intersections and shifts. + + >>> int(squares) + 72057594037928191 + + Also supports common set operations like + :func:`~chess.SquareSet.issubset()`, :func:`~chess.SquareSet.issuperset()`, + :func:`~chess.SquareSet.union()`, :func:`~chess.SquareSet.intersection()`, + :func:`~chess.SquareSet.difference()`, + :func:`~chess.SquareSet.symmetric_difference()` and + :func:`~chess.SquareSet.copy()` as well as + :func:`~chess.SquareSet.update()`, + :func:`~chess.SquareSet.intersection_update()`, + :func:`~chess.SquareSet.difference_update()`, + :func:`~chess.SquareSet.symmetric_difference_update()` and + :func:`~chess.SquareSet.clear()`. + """ + + def __init__(self, squares: IntoSquareSet = BB_EMPTY) -> None: + try: + self.mask = squares.__int__() & BB_ALL # type: ignore + return + except AttributeError: + self.mask = 0 + + # Try squares as an iterable. Not under except clause for nicer + # backtraces. + for square in squares: # type: ignore + self.add(square) + + # Set + + def __contains__(self, square: Square) -> bool: + return bool(BB_SQUARES[square] & self.mask) + + def __iter__(self) -> Iterator[Square]: + return scan_forward(self.mask) + + def __reversed__(self) -> Iterator[Square]: + return scan_reversed(self.mask) + + def __len__(self) -> int: + return popcount(self.mask) + + # MutableSet + + def add(self, square: Square) -> None: + """Adds a square to the set.""" + self.mask |= BB_SQUARES[square] + + def discard(self, square: Square) -> None: + """Discards a square from the set.""" + self.mask &= ~BB_SQUARES[square] + + # frozenset + + def isdisjoint(self, other: IntoSquareSet) -> bool: + """Tests if the square sets are disjoint.""" + return not bool(self & other) + + def issubset(self, other: IntoSquareSet) -> bool: + """Tests if this square set is a subset of another.""" + return not bool(~self & other) + + def issuperset(self, other: IntoSquareSet) -> bool: + """Tests if this square set is a superset of another.""" + return not bool(self & ~SquareSet(other)) + + def union(self, other: IntoSquareSet) -> SquareSet: + return self | other + + def __or__(self, other: IntoSquareSet) -> SquareSet: + r = SquareSet(other) + r.mask |= self.mask + return r + + def intersection(self, other: IntoSquareSet) -> SquareSet: + return self & other + + def __and__(self, other: IntoSquareSet) -> SquareSet: + r = SquareSet(other) + r.mask &= self.mask + return r + + def difference(self, other: IntoSquareSet) -> SquareSet: + return self - other + + def __sub__(self, other: IntoSquareSet) -> SquareSet: + r = SquareSet(other) + r.mask = self.mask & ~r.mask + return r + + def symmetric_difference(self, other: IntoSquareSet) -> SquareSet: + return self ^ other + + def __xor__(self, other: IntoSquareSet) -> SquareSet: + r = SquareSet(other) + r.mask ^= self.mask + return r + + def copy(self) -> SquareSet: + return SquareSet(self.mask) + + # set + + def update(self, *others: IntoSquareSet) -> None: + for other in others: + self |= other + + def __ior__(self, other: IntoSquareSet) -> SquareSet: + self.mask |= SquareSet(other).mask + return self + + def intersection_update(self, *others: IntoSquareSet) -> None: + for other in others: + self &= other + + def __iand__(self, other: IntoSquareSet) -> SquareSet: + self.mask &= SquareSet(other).mask + return self + + def difference_update(self, other: IntoSquareSet) -> None: + self -= other + + def __isub__(self, other: IntoSquareSet) -> SquareSet: + self.mask &= ~SquareSet(other).mask + return self + + def symmetric_difference_update(self, other: IntoSquareSet) -> None: + self ^= other + + def __ixor__(self, other: IntoSquareSet) -> SquareSet: + self.mask ^= SquareSet(other).mask + return self + + def remove(self, square: Square) -> None: + """ + Removes a square from the set. + + :raises: :exc:`KeyError` if the given *square* was not in the set. + """ + mask = BB_SQUARES[square] + if self.mask & mask: + self.mask ^= mask + else: + raise KeyError(square) + + def pop(self) -> Square: + """ + Removes and returns a square from the set. + + :raises: :exc:`KeyError` if the set is empty. + """ + if not self.mask: + raise KeyError("pop from empty SquareSet") + + square = lsb(self.mask) + self.mask &= (self.mask - 1) + return square + + def clear(self) -> None: + """Removes all elements from this set.""" + self.mask = BB_EMPTY + + # SquareSet + + def carry_rippler(self) -> Iterator[Bitboard]: + """Iterator over the subsets of this set.""" + return _carry_rippler(self.mask) + + def mirror(self) -> SquareSet: + """Returns a vertically mirrored copy of this square set.""" + return SquareSet(flip_vertical(self.mask)) + + def tolist(self) -> List[bool]: + """Converts the set to a list of 64 bools.""" + result = [False] * 64 + for square in self: + result[square] = True + return result + + def __bool__(self) -> bool: + return bool(self.mask) + + def __eq__(self, other: object) -> bool: + try: + return self.mask == SquareSet(other).mask # type: ignore + except (TypeError, ValueError): + return NotImplemented + + def __lshift__(self, shift: int) -> SquareSet: + return SquareSet((self.mask << shift) & BB_ALL) + + def __rshift__(self, shift: int) -> SquareSet: + return SquareSet(self.mask >> shift) + + def __ilshift__(self, shift: int) -> SquareSet: + self.mask = (self.mask << shift) & BB_ALL + return self + + def __irshift__(self, shift: int) -> SquareSet: + self.mask >>= shift + return self + + def __invert__(self) -> SquareSet: + return SquareSet(~self.mask & BB_ALL) + + def __int__(self) -> int: + return self.mask + + def __index__(self) -> int: + return self.mask + + def __repr__(self) -> str: + return f"SquareSet({self.mask:#021_x})" + + def __str__(self) -> str: + builder = [] + + for square in SQUARES_180: + mask = BB_SQUARES[square] + builder.append("1" if self.mask & mask else ".") + + if not mask & BB_FILE_H: + builder.append(" ") + elif square != H1: + builder.append("\n") + + return "".join(builder) + + def _repr_svg_(self) -> str: + import chess.svg + 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: + """ + Creates a :class:`~chess.SquareSet` from a single square. + + >>> import chess + >>> + >>> chess.SquareSet.from_square(chess.A1) == chess.BB_A1 + True + """ + return cls(BB_SQUARES[square]) diff --git a/cartesi-python-chess-cartesi-img/chess/__init__.pyc b/cartesi-python-chess-cartesi-img/chess/__init__.pyc new file mode 100644 index 0000000..2346e1d Binary files /dev/null and b/cartesi-python-chess-cartesi-img/chess/__init__.pyc differ diff --git a/cartesi-python-chess-cartesi-img/chess/_interactive.py b/cartesi-python-chess-cartesi-img/chess/_interactive.py new file mode 100644 index 0000000..edeef3d --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/_interactive.py @@ -0,0 +1,190 @@ +# TODO: Fix typing in this file. +# mypy: ignore-errors + +import chess.svg + + +class WidgetError(Exception): + """ + raised when ipywidgets is not installed + """ + + +class NotJupyter(Exception): + """ + raised when InteractiveViewer is instantiated from a non jupyter shell + """ + + +try: + from ipywidgets import Button, GridBox, Layout, HTML, Output, HBox, Select + from IPython.display import display, clear_output +except ModuleNotFoundError: + raise WidgetError("You need to have ipywidgets installed and running from Jupyter") + + +class InteractiveViewer: + def __new__(cls, game): + jupyter = True + try: + if get_ipython().__class__.__name__ != "ZMQInteractiveShell": + jupyter = False + except NameError: + jupyter = False + + if not jupyter: + raise NotJupyter("The interactive viewer only runs in Jupyter shell") + + return object.__new__(cls) + + def __init__(self, game): + self.game = game + self.__board = game.board() + self.__moves = list(game.mainline_moves()) + self.__white_moves = [str(move) for (i, move) in enumerate(self.__moves) if i % 2 == 0] + self.__black_moves = [str(move) for (i, move) in enumerate(self.__moves) if i % 2 == 1] + self.__move_list_len = len(self.__white_moves) + self.__num_moves = len(self.__moves) + self.__next_move = 0 if self.__moves else None + self.__out = Output() + + def __next_click(self, _): + move = self.__moves[self.__next_move] + self.__next_move += 1 + self.__board.push(move) + self.show() + + def __prev_click(self, _): + self.__board.pop() + self.__next_move -= 1 + self.show() + + def __reset_click(self, _): + self.__board.reset() + self.__next_move = 0 + self.show() + + def __white_select_change(self, change): + new = change["new"] + if (isinstance(new, dict)) and ("index" in new): + target = new["index"] * 2 + self.__seek(target) + self.show() + + def __black_select_change(self, change): + new = change["new"] + if (isinstance(new, dict)) and ("index" in new): + target = new["index"] * 2 + 1 + self.__seek(target) + self.show() + + def __seek(self, target): + while self.__next_move <= target: + move = self.__moves[self.__next_move] + self.__next_move += 1 + self.__board.push(move) + + while self.__next_move > target + 1: + self.__board.pop() + self.__next_move -= 1 + + def show(self): + display(self.__out) + next_move = Button( + icon="step-forward", + layout=Layout(width="60px", grid_area="right"), + disabled=self.__next_move >= self.__num_moves, + ) + + prev_move = Button( + icon="step-backward", + layout=Layout(width="60px", grid_area="left"), + disabled=self.__next_move == 0, + ) + + reset = Button( + icon="stop", + layout=Layout(width="60px", grid_area="middle"), + disabled=self.__next_move == 0, + ) + + if self.__next_move == 0: + white_move = None + black_move = None + else: + white_move = ( + self.__white_moves[self.__next_move // 2] + if (self.__next_move % 2) == 1 + else None + ) + black_move = ( + self.__black_moves[self.__next_move // 2 - 1] + if (self.__next_move % 2) == 0 + else None + ) + + white_move_list = Select( + options=self.__white_moves, + value=white_move, + rows=max(self.__move_list_len, 24), + disabled=False, + layout=Layout(width="80px"), + ) + + black_move_list = Select( + options=self.__black_moves, + value=black_move, + rows=max(self.__move_list_len, 24), + disabled=False, + layout=Layout(width="80px"), + ) + + white_move_list.observe(self.__white_select_change) + black_move_list.observe(self.__black_select_change) + + move_number_width = 3 + len(str(self.__move_list_len)) * 10 + + move_number = Select( + options=range(1, self.__move_list_len + 1), + value=None, + disabled=True, + rows=max(self.__move_list_len, 24), + layout=Layout(width=f"{move_number_width}px"), + ) + + move_list = HBox( + [move_number, white_move_list, black_move_list], + layout=Layout(height="407px", grid_area="moves"), + ) + + next_move.on_click(self.__next_click) + prev_move.on_click(self.__prev_click) + reset.on_click(self.__reset_click) + + with self.__out: + grid_box = GridBox( + children=[next_move, prev_move, reset, self.svg, move_list], + layout=Layout( + width=f"{390+move_number_width+160}px", + grid_template_rows="90% 10%", + grid_template_areas=""" + "top top top top top moves" + ". left middle right . moves" + """, + ), + ) + clear_output(wait=True) + display(grid_box) + + @property + def svg(self) -> HTML: + svg = chess.svg.board( + board=self.__board, + size=390, + lastmove=self.__board.peek() if self.__board.move_stack else None, + check=self.__board.king(self.__board.turn) + if self.__board.is_check() + else None, + ) + svg_widget = HTML(value=svg, layout=Layout(grid_area="top")) + return svg_widget diff --git a/cartesi-python-chess-cartesi-img/chess/engine.py b/cartesi-python-chess-cartesi-img/chess/engine.py new file mode 100644 index 0000000..3901900 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/engine.py @@ -0,0 +1,2909 @@ +# This file is part of the python-chess library. +# Copyright (C) 2012-2021 Niklas Fiekas +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import annotations + +import abc +import asyncio +import collections +import concurrent.futures +import contextlib +import copy +import dataclasses +import enum +import logging +import math +import warnings +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, Callable, Coroutine, Deque, Dict, Generator, Generic, Iterable, Iterator, List, Mapping, MutableMapping, Optional, Tuple, Type, TypeVar, Union + +try: + from typing import Literal + _WdlModel = Literal["sf", "sf14", "sf12", "lichess"] +except ImportError: + # Before Python 3.8. + _WdlModel = str # type: ignore + + +T = TypeVar("T") +ProtocolT = TypeVar("ProtocolT", bound="Protocol") + +ConfigValue = Union[str, int, bool, None] +ConfigMapping = Mapping[str, ConfigValue] + + +LOGGER = logging.getLogger(__name__) + + +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) -> asyncio.AbstractEventLoop: + 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: Optional[asyncio.AbstractEventLoop]) -> None: + 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) -> asyncio.AbstractEventLoop: + return asyncio.ProactorEventLoop() if sys.platform == "win32" else asyncio.SelectorEventLoop() # type: ignore + + def get_child_watcher(self) -> asyncio.AbstractChildWatcher: + 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: Optional[asyncio.AbstractChildWatcher]) -> None: + 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) -> asyncio.AbstractChildWatcher: + if sys.platform == "win32": + raise NotImplementedError + + try: + os.close(os.pidfd_open(os.getpid())) # type: ignore + watcher: asyncio.AbstractChildWatcher = asyncio.PidfdChildWatcher() # type: ignore + LOGGER.debug("Using PidfdChildWatcher") + return watcher + 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: + watcher = asyncio.ThreadedChildWatcher() + LOGGER.debug("Using ThreadedChildWatcher") + return watcher + except AttributeError: + # Before Python 3.8. + LOGGER.debug("Using SafeChildWatcher") + 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) + + LOGGER.debug("Using PollingChildWatcher") + 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: + """ + Runs ``coroutine(future)`` in a new event loop on a background thread. + + Blocks on *future* and returns the result as soon as it is resolved. + The coroutine and all remaining tasks continue running in the background + until complete. + + Note: This installs a :class:`chess.engine.EventLoopPolicy` for the entire + process. + """ + assert asyncio.iscoroutinefunction(coroutine) + + with _policy_lock: + if not isinstance(asyncio.get_event_loop_policy(), EventLoopPolicy): + asyncio.set_event_loop_policy(EventLoopPolicy()) + + future: concurrent.futures.Future[T] = concurrent.futures.Future() + + def background() -> None: + try: + asyncio.run(coroutine(future)) + future.cancel() + except Exception as exc: + future.set_exception(exc) + + threading.Thread(target=background, name=name).start() + return future.result() + + +class EngineError(RuntimeError): + """Runtime error caused by a misbehaving engine or incorrect usage.""" + + +class EngineTerminatedError(EngineError): + """The engine process exited unexpectedly.""" + + +class AnalysisComplete(Exception): + """ + Raised when analysis is complete, all information has been consumed, but + further information was requested. + """ + + +@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": + return value and value != "false" + elif self.type == "spin": + try: + value = int(value) # type: ignore + except ValueError: + raise EngineError(f"expected integer for spin option {self.name!r}, got: {value!r}") + if self.min is not None and value < self.min: + raise EngineError(f"expected value for option {self.name!r} to be at least {self.min}, got: {value}") + if self.max is not None and self.max < value: + raise EngineError(f"expected value for option {self.name!r} to be at most {self.max}, got: {value}") + return value + elif self.type == "combo": + value = str(value) + if value not in (self.var or []): + raise EngineError("invalid value for combo option {!r}, got: {} (available: {})".format(self.name, value, ", ".join(self.var) if self.var else "-")) + return value + elif self.type in ["button", "reset", "save"]: + return None + elif self.type in ["string", "file", "path"]: + value = str(value) + if "\n" in value or "\r" in value: + raise EngineError(f"invalid line-break in string option {self.name!r}: {value!r}") + return value + else: + raise EngineError(f"unknown option type: {self.type!r}") + + def is_managed(self) -> bool: + """ + Some options are managed automatically: ``UCI_Chess960``, + ``UCI_Variant``, ``MultiPV``, ``Ponder``. + """ + return self.name.lower() in MANAGED_OPTIONS + + +@dataclasses.dataclass +class Limit: + """Search-termination condition.""" + + time: Optional[float] = None + """Search exactly *time* seconds.""" + + depth: Optional[int] = None + """Search *depth* ply only.""" + + nodes: Optional[int] = None + """Search only a limited number of *nodes*.""" + + mate: Optional[int] = None + """Search for a mate in *mate* moves.""" + + white_clock: Optional[float] = None + """Time in seconds remaining for White.""" + + black_clock: Optional[float] = None + """Time in seconds remaining for Black.""" + + white_inc: Optional[float] = None + """Fisher increment for White, in seconds.""" + + black_inc: Optional[float] = None + """Fisher increment for Black, in seconds.""" + + remaining_moves: Optional[int] = None + """ + Number of moves to the next time control. If this is not set, but + *white_clock* and *black_clock* are, then it is sudden death. + """ + + def __repr__(self) -> str: + # Like default __repr__, but without None values. + return "{}({})".format( + type(self).__name__, + ", ".join("{}={!r}".format(attr, getattr(self, attr)) + for attr in ["time", "depth", "nodes", "mate", "white_clock", "black_clock", "white_inc", "black_inc", "remaining_moves"] + if getattr(self, attr) is not None)) + + +try: + class InfoDict(typing.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 +except AttributeError: + # Before Python 3.8. + InfoDict = dict # type: ignore + + +class PlayResult: + """Returned by :func:`chess.engine.Protocol.play()`.""" + + move: Optional[chess.Move] + """The best move according to the engine, or ``None``.""" + + ponder: Optional[chess.Move] + """The response that the engine expects after *move*, or ``None``.""" + + info: InfoDict + """ + A dictionary of extra :class:`information ` + sent by the engine, if selected with the *info* argument of + :func:`~chess.engine.Protocol.play()`. + """ + + draw_offered: bool + """Whether the engine offered a draw before moving.""" + + resigned: bool + """Whether the engine resigned.""" + + def __init__(self, + move: Optional[chess.Move], + ponder: Optional[chess.Move], + info: Optional[InfoDict] = None, + *, + draw_offered: bool = False, + resigned: bool = False) -> None: + self.move = move + self.ponder = ponder + self.info = info or {} + self.draw_offered = draw_offered + self.resigned = resigned + + def __repr__(self) -> str: + return "<{} at {:#x} (move={}, ponder={}, info={}, draw_offered={}, resigned={})>".format( + type(self).__name__, id(self), self.move, self.ponder, self.info, + self.draw_offered, self.resigned) + + +class Info(enum.IntFlag): + """Used to filter information sent by the chess engine.""" + NONE = 0 + BASIC = 1 + SCORE = 2 + PV = 4 + REFUTATION = 8 + CURRLINE = 16 + ALL = BASIC | SCORE | PV | REFUTATION | CURRLINE + +INFO_NONE = Info.NONE +INFO_BASIC = Info.BASIC +INFO_SCORE = Info.SCORE +INFO_PV = Info.PV +INFO_REFUTATION = Info.REFUTATION +INFO_CURRLINE = Info.CURRLINE +INFO_ALL = Info.ALL + + +class PovScore: + """A relative :class:`~chess.engine.Score` and the point of view.""" + + relative: Score + """The relative :class:`~chess.engine.Score`.""" + + turn: Color + """The point of view (``chess.WHITE`` or ``chess.BLACK``).""" + + def __init__(self, relative: Score, turn: Color) -> None: + self.relative = relative + self.turn = turn + + def white(self) -> Score: + """Gets the score from White's point of view.""" + return self.pov(chess.WHITE) + + def black(self) -> Score: + """Gets the score from Black's point of view.""" + return self.pov(chess.BLACK) + + def pov(self, color: Color) -> Score: + """Gets the score from the point of view of the given *color*.""" + return self.relative if self.turn == color else -self.relative + + def is_mate(self) -> bool: + """Tests if this is a mate score.""" + return self.relative.is_mate() + + def wdl(self, *, model: _WdlModel = "sf", ply: int = 30) -> PovWdl: + """See :func:`~chess.engine.Score.wdl()`.""" + return PovWdl(self.relative.wdl(), self.turn) + + def __repr__(self) -> str: + return "PovScore({!r}, {})".format(self.relative, "WHITE" if self.turn else "BLACK") + + def __eq__(self, other: object) -> bool: + if isinstance(other, PovScore): + return self.white() == other.white() + else: + return NotImplemented + + +class Score(abc.ABC): + """ + Evaluation of a position. + + The score can be :class:`~chess.engine.Cp` (centi-pawns), + :class:`~chess.engine.Mate` or :py:data:`~chess.engine.MateGiven`. + A positive value indicates an advantage. + + There is a total order defined on centi-pawn and mate scores. + + >>> from chess.engine import Cp, Mate, MateGiven + >>> + >>> Mate(-0) < Mate(-1) < Cp(-50) < Cp(200) < Mate(4) < Mate(1) < MateGiven + True + + Scores can be negated to change the point of view: + + >>> -Cp(20) + Cp(-20) + + >>> -Mate(-4) + Mate(+4) + + >>> -Mate(0) + MateGiven + """ + + @typing.overload + def score(self, *, mate_score: int) -> int: ... + @typing.overload + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: ... + @abc.abstractmethod + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: + """ + Returns the centi-pawn score as an integer or ``None``. + + You can optionally pass a large value to convert mate scores to + centi-pawn scores. + + >>> Cp(-300).score() + -300 + >>> Mate(5).score() is None + True + >>> Mate(5).score(mate_score=100000) + 99995 + """ + + @abc.abstractmethod + def mate(self) -> Optional[int]: + """ + Returns the number of plies to mate, negative if we are getting + mated, or ``None``. + + .. warning:: + This conflates ``Mate(0)`` (we lost) and ``MateGiven`` + (we won) to ``0``. + """ + + def is_mate(self) -> bool: + """Tests if this is a mate score.""" + return self.mate() is not None + + @abc.abstractmethod + def wdl(self, *, model: _WdlModel = "sf", ply: int = 30) -> Wdl: + """ + Returns statistics for the expected outcome of this game, based on + a *model*, given that this score is reached at *ply*. + + Scores have a total order, but it makes little sense to compute + the difference between two scores. For example, going from + ``Cp(-100)`` to ``Cp(+100)`` is much more significant than going + from ``Cp(+300)`` to ``Cp(+500)``. It is better to compute differences + of the expectation values for the outcome of the game (based on winning + chances and drawing chances). + + >>> Cp(100).wdl().expectation() - Cp(-100).wdl().expectation() # doctest: +ELLIPSIS + 0.379... + + >>> Cp(500).wdl().expectation() - Cp(300).wdl().expectation() # doctest: +ELLIPSIS + 0.015... + + :param model: + * ``sf``, the WDL model used by the latest Stockfish + (currently ``sf14``). + * ``sf14``, the WDL model used by Stockfish 14. + * ``sf12``, the WDL model used by Stockfish 12. + * ``lichess``, the win rate model used by Lichess. + Does not use *ply*, and does not consider drawing chances. + :param ply: The number of half-moves played since the starting + position. Models may scale scores slightly differently based on + this. Defaults to middle game. + """ + + @abc.abstractmethod + def __neg__(self) -> Score: + ... + + @abc.abstractmethod + def __pos__(self) -> Score: + ... + + @abc.abstractmethod + def __abs__(self) -> Score: + ... + + def _score_tuple(self) -> Tuple[bool, bool, bool, int, Optional[int]]: + mate = self.mate() + return ( + isinstance(self, MateGivenType), + mate is not None and mate > 0, + mate is None, + -(mate or 0), + self.score(), + ) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() == other._score_tuple() + else: + return NotImplemented + + def __lt__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() < other._score_tuple() + else: + return NotImplemented + + def __le__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() <= other._score_tuple() + else: + return NotImplemented + + def __gt__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() > other._score_tuple() + else: + return NotImplemented + + def __ge__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() >= other._score_tuple() + else: + return NotImplemented + + +def _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: + return round(1000 / (1 + math.exp(-0.004 * cp))) + + +class Cp(Score): + """Centi-pawn score.""" + + def __init__(self, cp: int) -> None: + self.cp = cp + + def mate(self) -> None: + return 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) + else: + wins = _sf14_wins(self.cp, ply=ply) + losses = _sf14_wins(-self.cp, ply=ply) + draws = 1000 - wins - losses + return Wdl(wins, draws, losses) + + def __str__(self) -> str: + return f"+{self.cp:d}" if self.cp > 0 else str(self.cp) + + def __repr__(self) -> str: + return f"Cp({self})" + + def __neg__(self) -> Cp: + return Cp(-self.cp) + + def __pos__(self) -> Cp: + return Cp(self.cp) + + def __abs__(self) -> Cp: + return Cp(abs(self.cp)) + + +class Mate(Score): + """Mate score.""" + + def __init__(self, moves: int) -> None: + self.moves = moves + + def mate(self) -> int: + return self.moves + + @typing.overload + def score(self, *, mate_score: int) -> int: ... + @typing.overload + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: ... + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: + if mate_score is None: + return None + elif self.moves > 0: + return mate_score - self.moves + else: + return -mate_score - self.moves + + def wdl(self, *, model: _WdlModel = "sf", ply: int = 30) -> Wdl: + if model == "lichess": + cp = (21 - min(10, abs(self.moves))) * 100 + wins = _lichess_raw_wins(cp) + return Wdl(wins, 0, 1000 - wins) if self.moves > 0 else Wdl(1000 - wins, 0, wins) + else: + return Wdl(1000, 0, 0) if self.moves > 0 else Wdl(0, 0, 1000) + + def __str__(self) -> str: + return f"#+{self.moves}" if self.moves > 0 else f"#-{abs(self.moves)}" + + def __repr__(self) -> str: + return "Mate({})".format(str(self).lstrip("#")) + + def __neg__(self) -> Union[MateGivenType, Mate]: + return MateGiven if not self.moves else Mate(-self.moves) + + def __pos__(self) -> Mate: + return Mate(self.moves) + + def __abs__(self) -> Union[MateGivenType, Mate]: + return MateGiven if not self.moves else Mate(abs(self.moves)) + + +class MateGivenType(Score): + """Winning mate score, equivalent to ``-Mate(0)``.""" + + def mate(self) -> int: + return 0 + + @typing.overload + def score(self, *, mate_score: int) -> int: ... + @typing.overload + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: ... + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: + return mate_score + + def wdl(self, *, model: _WdlModel = "sf", ply: int = 30) -> Wdl: + return Wdl(1000, 0, 0) + + def __neg__(self) -> Mate: + return Mate(0) + + def __pos__(self) -> MateGivenType: + return self + + def __abs__(self) -> MateGivenType: + return self + + def __repr__(self) -> str: + return "MateGiven" + + def __str__(self) -> str: + return "#+0" + +MateGiven = MateGivenType() + + +class PovWdl: + """ + Relative :class:`win/draw/loss statistics ` and the point + of view. + + .. deprecated:: 1.2 + Behaves like a tuple + ``(wdl.relative.wins, wdl.relative.draws, wdl.relative.losses)`` + for backwards compatibility. But it is recommended to use the provided + fields and methods instead. + """ + + relative: Wdl + """The relative :class:`~chess.engine.Wdl`.""" + + turn: Color + """The point of view (``chess.WHITE`` or ``chess.BLACK``).""" + + def __init__(self, relative: Wdl, turn: Color) -> None: + self.relative = relative + self.turn = turn + + 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") + + # Unfortunately in python-chess v1.1.0, info["wdl"] was a simple tuple + # of the relative permille values, so we have to support __iter__, + # __len__, __getitem__, and equality comparisons with other tuples. + # Never mind the ordering, because that's not a sensible operation, anyway. + + def __iter__(self) -> Iterator[int]: + yield self.relative.wins + yield self.relative.draws + yield self.relative.losses + + def __len__(self) -> int: + return 3 + + def __getitem__(self, idx: int) -> int: + return (self.relative.wins, self.relative.draws, self.relative.losses)[idx] + + def __eq__(self, other: object) -> bool: + if isinstance(other, PovWdl): + return self.white() == other.white() + elif isinstance(other, tuple): + return (self.relative.wins, self.relative.draws, self.relative.losses) == other + else: + return NotImplemented + + +@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 __iter__(self) -> Iterator[int]: + yield self.wins + yield self.draws + yield self.losses + + def __reversed__(self) -> Iterator[int]: + yield self.losses + yield self.draws + yield self.wins + + def __pos__(self) -> Wdl: + return self + + def __neg__(self) -> Wdl: + return Wdl(self.losses, self.draws, self.wins) + + +class MockTransport(asyncio.SubprocessTransport, asyncio.WriteTransport): + def __init__(self, protocol: Protocol) -> None: + super().__init__() + self.protocol = protocol + self.expectations: Deque[Tuple[str, List[str]]] = collections.deque() + self.expected_pings = 0 + self.stdin_buffer = bytearray() + self.protocol.connection_made(self) + + def expect(self, expectation: str, responses: List[str] = []) -> None: + self.expectations.append((expectation, responses)) + + def expect_ping(self) -> None: + self.expected_pings += 1 + + def assert_done(self) -> None: + assert not self.expectations, f"pending expectations: {self.expectations}" + + def get_pipe_transport(self, fd: int) -> Optional[asyncio.BaseTransport]: + assert fd == 0, f"expected 0 for stdin, got {fd}" + return self + + def write(self, data: bytes) -> None: + self.stdin_buffer.extend(data) + while b"\n" in self.stdin_buffer: + line_bytes, self.stdin_buffer = self.stdin_buffer.split(b"\n", 1) + line = line_bytes.decode("utf-8") + + if line.startswith("ping ") and self.expected_pings: + self.expected_pings -= 1 + self.protocol.pipe_data_received(1, line.replace("ping ", "pong ").encode("utf-8") + b"\n") + else: + assert self.expectations, f"unexpected: {line!r}" + expectation, responses = self.expectations.popleft() + assert expectation == line, f"expected {expectation}, got: {line}" + if responses: + self.protocol.pipe_data_received(1, "\n".join(responses).encode("utf-8") + b"\n") + + def get_pid(self) -> int: + return id(self) + + def get_returncode(self) -> Optional[int]: + return None if self.expectations else 0 + + +class Protocol(asyncio.SubprocessProtocol, metaclass=abc.ABCMeta): + """Protocol for communicating with a chess engine process.""" + + options: MutableMapping[str, Option] + """Dictionary of available options.""" + + id: Dict[str, str] + """ + Dictionary of information about the engine. Common keys are ``name`` + and ``author``. + """ + + returncode: asyncio.Future[int] + """Future: Exit code of the process.""" + + def __init__(self: ProtocolT) -> None: + self.loop = asyncio.get_running_loop() + self.transport: Optional[asyncio.SubprocessTransport] = None + + self.buffer = { + 1: bytearray(), # stdout + 2: bytearray(), # stderr + } + + self.command: Optional[BaseCommand[ProtocolT, Any]] = None + self.next_command: Optional[BaseCommand[ProtocolT, Any]] = None + + self.initialized = False + self.returncode: asyncio.Future[int] = asyncio.Future() + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + # SubprocessTransport expected, but not checked to allow duck typing. + self.transport = transport # type: ignore + LOGGER.debug("%s: Connection made", self) + + def connection_lost(self: ProtocolT, 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 + + self.returncode.set_result(code) + + def process_exited(self) -> None: + LOGGER.debug("%s: Process exited", self) + + def send_line(self, line: str) -> None: + LOGGER.debug("%s: << %s", self, line) + assert self.transport is not None, "cannot send line before connection is made" + stdin = self.transport.get_pipe_transport(0) + # WriteTransport expected, but not checked to allow duck typing. + stdin.write(line.encode("utf-8")) # type: ignore + stdin.write(b"\n") # type: ignore + + def pipe_data_received(self, fd: int, data: Union[bytes, str]) -> None: + self.buffer[fd].extend(data) # type: ignore + while b"\n" in self.buffer[fd]: + line_bytes, self.buffer[fd] = self.buffer[fd].split(b"\n", 1) + if line_bytes.endswith(b"\r"): + line_bytes = line_bytes[:-1] + try: + line = line_bytes.decode("utf-8") + except UnicodeDecodeError as err: + LOGGER.warning("%s: >> %r (%s)", self, bytes(line_bytes), err) + else: + if fd == 1: + self.loop.call_soon(self._line_received, line) + else: + self.loop.call_soon(self.error_line_received, line) + + def error_line_received(self, line: str) -> None: + LOGGER.warning("%s: stderr >> %s", self, line) + + def _line_received(self: ProtocolT, line: str) -> None: + LOGGER.debug("%s: >> %s", self, line) + + self.line_received(line) + + if self.command: + self.command._line_received(self, line) + + def line_received(self, line: str) -> None: + pass + + async def communicate(self: ProtocolT, command_factory: Callable[[ProtocolT], BaseCommand[ProtocolT, 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 + + if self.next_command is not None: + self.next_command.result.cancel() + self.next_command.finished.cancel() + self.next_command.set_finished() + + self.next_command = command + + def previous_command_finished(_: Optional[asyncio.Future[None]]) -> None: + self.command, self.next_command = self.next_command, None + if self.command is not None: + cmd = self.command + + def cancel_if_cancelled(result: asyncio.Future[T]) -> None: + if result.cancelled(): + cmd._cancel(self) + + cmd.result.add_done_callback(cancel_if_cancelled) + cmd.finished.add_done_callback(previous_command_finished) + cmd._start(self) + + if self.command is None: + previous_command_finished(None) + elif not self.command.result.done(): + self.command.result.cancel() + elif not self.command.result.cancelled(): + self.command._cancel(self) + + return await command.result + + def __repr__(self) -> str: + pid = self.transport.get_pid() if self.transport is not None else "?" + return f"<{type(self).__name__} (pid={pid})>" + + @abc.abstractmethod + async def initialize(self) -> None: + """Initializes the engine.""" + + @abc.abstractmethod + async def ping(self) -> None: + """ + Pings the engine and waits for a response. Used to ensure the engine + is still alive and idle. + """ + + @abc.abstractmethod + async def configure(self, options: ConfigMapping) -> None: + """ + Configures global engine options. + + :param options: A dictionary of engine options where the keys are + names of :data:`~chess.engine.Protocol.options`. Do not set options + that are managed automatically + (:func:`chess.engine.Option.is_managed()`). + """ + + @abc.abstractmethod + async def 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 = {}) -> PlayResult: + """ + Plays a position. + + :param board: The position. The entire move stack will be sent to the + engine. + :param limit: An instance of :class:`chess.engine.Limit` that + determines when to stop thinking. + :param game: Optional. An arbitrary object that identifies the game. + Will automatically inform the engine if the object is not equal + to the previous game (e.g., ``ucinewgame``, ``new``). + :param info: Selects which additional information to retrieve from the + engine. ``INFO_NONE``, ``INFO_BASE`` (basic information that is + trivial to obtain), ``INFO_SCORE``, ``INFO_PV``, + ``INFO_REFUTATION``, ``INFO_CURRLINE``, ``INFO_ALL`` or any + bitwise combination. Some overhead is associated with parsing + extra information. + :param ponder: Whether the engine should keep analysing in the + background even after the result has been returned. + :param draw_offered: Whether the engine's opponent has offered a draw. + Ignored by UCI engines. + :param root_moves: Optional. Consider only root moves from this list. + :param options: Optional. A dictionary of engine options for the + analysis. The previous configuration will be restored after the + analysis is complete. You can permanently apply a configuration + with :func:`~chess.engine.Protocol.configure()`. + """ + + @typing.overload + async def analyse(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> InfoDict: ... + @typing.overload + async def analyse(self, board: chess.Board, limit: Limit, *, multipv: int, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> List[InfoDict]: ... + @typing.overload + async def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> Union[List[InfoDict], InfoDict]: ... + async def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> Union[List[InfoDict], InfoDict]: + """ + Analyses a position and returns a dictionary of + :class:`information `. + + :param board: The position to analyse. The entire move stack will be + sent to the engine. + :param limit: An instance of :class:`chess.engine.Limit` that + determines when to stop the analysis. + :param multipv: Optional. Analyse multiple root moves. Will return + a list of at most *multipv* dictionaries rather than just a single + info dictionary. + :param game: Optional. An arbitrary object that identifies the game. + Will automatically inform the engine if the object is not equal + to the previous game (e.g., ``ucinewgame``, ``new``). + :param info: Selects which information to retrieve from the + engine. ``INFO_NONE``, ``INFO_BASE`` (basic information that is + trivial to obtain), ``INFO_SCORE``, ``INFO_PV``, + ``INFO_REFUTATION``, ``INFO_CURRLINE``, ``INFO_ALL`` or any + bitwise combination. Some overhead is associated with parsing + extra information. + :param root_moves: Optional. Limit analysis to a list of root moves. + :param options: Optional. A dictionary of engine options for the + analysis. The previous configuration will be restored after the + analysis is complete. You can permanently apply a configuration + with :func:`~chess.engine.Protocol.configure()`. + """ + analysis = await self.analysis(board, limit, multipv=multipv, game=game, info=info, root_moves=root_moves, options=options) + + with analysis: + await analysis.wait() + + return analysis.info if multipv is None else analysis.multipv + + @abc.abstractmethod + async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> AnalysisResult: + """ + Starts analysing a position. + + :param board: The position to analyse. The entire move stack will be + sent to the engine. + :param limit: Optional. An instance of :class:`chess.engine.Limit` + that determines when to stop the analysis. Analysis is infinite + by default. + :param multipv: Optional. Analyse multiple root moves. + :param game: Optional. An arbitrary object that identifies the game. + Will automatically inform the engine if the object is not equal + to the previous game (e.g., ``ucinewgame``, ``new``). + :param info: Selects which information to retrieve from the + engine. ``INFO_NONE``, ``INFO_BASE`` (basic information that is + trivial to obtain), ``INFO_SCORE``, ``INFO_PV``, + ``INFO_REFUTATION``, ``INFO_CURRLINE``, ``INFO_ALL`` or any + bitwise combination. Some overhead is associated with parsing + extra information. + :param root_moves: Optional. Limit analysis to a list of root moves. + :param options: Optional. A dictionary of engine options for the + analysis. The previous configuration will be restored after the + analysis is complete. You can permanently apply a configuration + with :func:`~chess.engine.Protocol.configure()`. + + Returns :class:`~chess.engine.AnalysisResult`, a handle that allows + asynchronously iterating over the information sent by the engine + and stopping the analysis at any time. + """ + + @abc.abstractmethod + async def quit(self) -> None: + """Asks the engine to shut down.""" + + @classmethod + async def popen(cls: Type[ProtocolT], command: Union[str, List[str]], *, setpgrp: bool = False, **popen_args: Any) -> Tuple[asyncio.SubprocessTransport, ProtocolT]: + if not isinstance(command, list): + command = [command] + + if setpgrp: + try: + # Windows. + popen_args["creationflags"] = popen_args.get("creationflags", 0) | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore + except AttributeError: + # Unix. + popen_args["start_new_session"] = True + + return await asyncio.get_running_loop().subprocess_exec(cls, *command, **popen_args) # type: ignore + + +class CommandState(enum.Enum): + NEW = enum.auto() + ACTIVE = enum.auto() + CANCELLING = enum.auto() + DONE = enum.auto() + + +class BaseCommand(Generic[ProtocolT, T]): + def __init__(self, engine: ProtocolT) -> None: + self.state = CommandState.NEW + + self.result: asyncio.Future[T] = asyncio.Future() + self.finished: asyncio.Future[None] = asyncio.Future() + + def _engine_terminated(self, engine: ProtocolT, 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.set_result(None) + elif self.state == CommandState.NEW: + self._handle_exception(engine, exc) + + def _handle_exception(self, engine: ProtocolT, exc: Exception) -> None: + if not self.result.done(): + self.result.set_exception(exc) + else: + engine.loop.call_exception_handler({ + "message": f"{type(self).__name__} failed after returning preliminary result ({self.result!r})", + "exception": exc, + "protocol": engine, + "transport": engine.transport, + }) + + if not self.finished.done(): + self.finished.set_result(None) + + def set_finished(self) -> None: + assert self.state in [CommandState.ACTIVE, CommandState.CANCELLING] + if not self.result.done(): + self.result.set_exception(EngineError(f"engine command finished before returning result: {self!r}")) + self.finished.set_result(None) + self.state = CommandState.DONE + + def _cancel(self, engine: ProtocolT) -> None: + if self.state != CommandState.CANCELLING and self.state != CommandState.DONE: + assert self.state == CommandState.ACTIVE + self.state = CommandState.CANCELLING + self.cancel(engine) + + def _start(self, engine: ProtocolT) -> None: + assert self.state == CommandState.NEW + self.state = CommandState.ACTIVE + try: + self.check_initialized(engine) + self.start(engine) + except EngineError as err: + self._handle_exception(engine, err) + + def _line_received(self, engine: ProtocolT, line: str) -> None: + assert self.state in [CommandState.ACTIVE, CommandState.CANCELLING] + try: + self.line_received(engine, line) + except EngineError as err: + self._handle_exception(engine, err) + + def cancel(self, engine: ProtocolT) -> None: + pass + + def check_initialized(self, engine: ProtocolT) -> None: + if not engine.initialized: + raise EngineError("tried to run command, but engine is not initialized") + + def start(self, engine: ProtocolT) -> None: + raise NotImplementedError + + def line_received(self, engine: ProtocolT, line: str) -> None: + pass + + def engine_terminated(self, engine: ProtocolT, exc: Exception) -> None: + self._handle_exception(engine, exc) + + def __repr__(self) -> str: + return "<{} at {:#x} (state={}, result={}, finished={}>".format(type(self).__name__, id(self), self.state, self.result, self.finished) + + +class UciProtocol(Protocol): + """ + An implementation of the + `Universal Chess Interface `_ + protocol. + """ + + def __init__(self) -> None: + super().__init__() + self.options: UciOptionMap[Option] = UciOptionMap() + self.config: UciOptionMap[ConfigValue] = UciOptionMap() + self.target_config: UciOptionMap[ConfigValue] = UciOptionMap() + self.id = {} + self.board = chess.Board() + self.game: object = None + self.first_game = True + self.may_ponderhit: Optional[chess.Board] = None + self.ponderhit = False + + async def initialize(self) -> None: + class UciInitializeCommand(BaseCommand[UciProtocol, None]): + def check_initialized(self, engine: UciProtocol) -> None: + if engine.initialized: + raise EngineError("engine already initialized") + + def start(self, engine: UciProtocol) -> None: + engine.send_line("uci") + + def line_received(self, engine: UciProtocol, line: str) -> None: + if line == "uciok" and not self.result.done(): + 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]) + + def _option(self, engine: UciProtocol, arg: str) -> None: + current_parameter = None + + name: List[str] = [] + type: List[str] = [] + default: List[str] = [] + min = None + max = None + current_var = None + 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) + 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") + + if current_var is not None: + var.append(" ".join(current_var)) + + 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 + + if option.default is not None: + 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 + + def _id(self, engine: UciProtocol, arg: str) -> None: + key, value = arg.split(" ", 1) + engine.id[key] = value + + return await self.communicate(UciInitializeCommand) + + def _isready(self) -> None: + self.send_line("isready") + + def _ucinewgame(self) -> None: + self.send_line("ucinewgame") + self.first_game = False + self.ponderhit = False + + def debug(self, on: bool = True) -> None: + """ + Switches debug mode of the engine on or off. This does not interrupt + other ongoing operations. + """ + if on: + self.send_line("debug on") + else: + self.send_line("debug off") + + async def ping(self) -> None: + class UciPingCommand(BaseCommand[UciProtocol, None]): + def start(self, engine: UciProtocol) -> None: + engine._isready() + + def line_received(self, engine: UciProtocol, line: str) -> None: + if line == "readyok": + self.result.set_result(None) + self.set_finished() + else: + LOGGER.warning("%s: Unexpected engine output: %r", 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 collections.ChainMap(options, self.target_config).items()) + + def _setoption(self, name: str, value: ConfigValue) -> None: + try: + value = self.options[name].parse(value) + except KeyError: + raise EngineError("engine does not support option {} (available options: {})".format(name, ", ".join(self.options))) + + if value is None or value != self.config.get(name): + builder = ["setoption name", name] + if value is False: + builder.append("value false") + elif value is True: + builder.append("value true") + elif value is not None: + builder.append("value") + builder.append(str(value)) + + 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(): + 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}) + self.result.set_result(None) + self.set_finished() + + return await self.communicate(UciConfigureCommand) + + def _position(self, board: chess.Board) -> None: + # Select UCI_Variant and UCI_Chess960. + uci_variant = type(board).uci_variant + if "UCI_Variant" in self.options: + self._setoption("UCI_Variant", uci_variant) + elif uci_variant != "chess": + raise EngineError("engine does not support UCI_Variant") + + if "UCI_Chess960" in self.options: + self._setoption("UCI_Chess960", board.chess960) + elif board.chess960: + raise EngineError("engine does not support UCI_Chess960") + + # Send starting position. + builder = ["position"] + safe_history = all(board.move_stack) + root = board.root() if safe_history else board + fen = root.fen(shredder=board.chess960, en_passant="fen") + if uci_variant == "chess" and fen == chess.STARTING_FEN: + builder.append("startpos") + else: + builder.append("fen") + builder.append(fen) + + # Send moves. + if not safe_history: + LOGGER.warning("Not transmitting history with null moves to UCI engine") + elif board.move_stack: + builder.append("moves") + builder.extend(move.uci() for move in board.move_stack) + + self.send_line(" ".join(builder)) + self.board = board.copy(stack=False) + + def _go(self, limit: Limit, *, root_moves: Optional[Iterable[chess.Move]] = None, ponder: bool = False, infinite: bool = False) -> None: + builder = ["go"] + if ponder: + builder.append("ponder") + if limit.white_clock is not None: + builder.append("wtime") + builder.append(str(max(1, int(limit.white_clock * 1000)))) + if limit.black_clock is not None: + builder.append("btime") + builder.append(str(max(1, int(limit.black_clock * 1000)))) + if limit.white_inc is not None: + builder.append("winc") + builder.append(str(int(limit.white_inc * 1000))) + if limit.black_inc is not None: + builder.append("binc") + builder.append(str(int(limit.black_inc * 1000))) + if limit.remaining_moves is not None and int(limit.remaining_moves) > 0: + builder.append("movestogo") + builder.append(str(int(limit.remaining_moves))) + if limit.depth is not None: + builder.append("depth") + builder.append(str(max(1, int(limit.depth)))) + if limit.nodes is not None: + builder.append("nodes") + builder.append(str(max(1, int(limit.nodes)))) + if limit.mate is not None: + builder.append("mate") + builder.append(str(max(1, int(limit.mate)))) + if limit.time is not None: + builder.append("movetime") + builder.append(str(max(1, int(limit.time * 1000)))) + if infinite: + builder.append("infinite") + if root_moves is not None: + builder.append("searchmoves") + if root_moves: + builder.extend(move.uci() for move in root_moves) + else: + # Work around searchmoves followed by nothing. + builder.append("0000") + self.send_line(" ".join(builder)) + + async def play(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_NONE, ponder: bool = False, draw_offered: bool = False, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> PlayResult: + same_game = not self.first_game and game == self.game and not options + self.last_move = board.move_stack[-1] if (same_game and ponder and board.move_stack) else chess.Move.null() + + class UciPlayCommand(BaseCommand[UciProtocol, PlayResult]): + def __init__(self, engine: UciProtocol): + super().__init__(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(options) else None + + def start(self, engine: UciProtocol) -> None: + self.info: InfoDict = {} + self.pondering: Optional[chess.Board] = None + self.sent_isready = False + self.start_time = time.perf_counter() + + if engine.ponderhit: + engine.ponderhit = False + engine.send_line("ponderhit") + return + + 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) + + engine._configure(options) + + if engine.first_game or engine.game != game: + engine.game = game + engine._ucinewgame() + self.sent_isready = True + 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) + else: + LOGGER.warning("%s: Unexpected engine output: %r", engine, line) + + def _readyok(self, engine: UciProtocol) -> None: + self.sent_isready = False + engine._position(board) + engine._go(limit, root_moves=root_moves) + + def _info(self, engine: UciProtocol, arg: str) -> None: + if not self.pondering: + self.info.update(_parse_uci_info(arg, engine.board, info)) + + def _bestmove(self, engine: UciProtocol, arg: str) -> None: + if self.pondering: + self.pondering = None + elif not self.result.cancelled(): + best = _parse_uci_bestmove(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) + 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 + + engine._go(ponder_limit, ponder=True) + + if not self.pondering: + self.end(engine) + + def end(self, engine: UciProtocol) -> None: + engine.may_ponderhit = None + self.set_finished() + + def cancel(self, engine: UciProtocol) -> None: + if engine.may_ponderhit and self.pondering and engine.may_ponderhit.move_stack == self.pondering.move_stack and engine.may_ponderhit == self.pondering: + engine.ponderhit = True + self.end(engine) + else: + engine.send_line("stop") + + def engine_terminated(self, engine: UciProtocol, exc: Exception) -> None: + # Allow terminating engine while pondering. + if not self.result.done(): + super().engine_terminated(engine, 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)) + self.sent_isready = False + + if "Ponder" in engine.options: + engine._setoption("Ponder", 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) + + engine._configure(options) + + if engine.first_game or engine.game != game: + engine.game = game + engine._ucinewgame() + self.sent_isready = True + 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) + else: + LOGGER.warning("%s: Unexpected engine output: %r", engine, line) + + def _readyok(self, engine: UciProtocol) -> None: + self.sent_isready = False + engine._position(board) + + if limit: + engine._go(limit, root_moves=root_moves) + else: + 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 _bestmove(self, engine: UciProtocol, arg: str) -> None: + if not self.result.done(): + raise EngineError("was not searching, but engine sent bestmove") + best = _parse_uci_bestmove(engine.board, arg) + self.set_finished() + self.analysis.set_finished(best) + + def cancel(self, engine: UciProtocol) -> None: + 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) + self.analysis.set_exception(exc) + + return await self.communicate(UciAnalysisCommand) + + async def quit(self) -> None: + self.send_line("quit") + await asyncio.shield(self.returncode) + + +UCI_REGEX = re.compile(r"^[a-h][1-8][a-h][1-8][pnbrqk]?|[PNBRQK]@[a-h][1-8]|0000\Z") + +def _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) + + if parameter == "string": + info["string"] = " ".join(tokens) + break + elif parameter in ["depth", "seldepth", "nodes", "multipv", "currmovenumber", "hashfull", "nps", "tbhits", "cpuload"]: + try: + info[parameter] = int(tokens.pop(0)) # type: ignore + except (ValueError, IndexError): + LOGGER.error("Exception parsing %s from info: %r", parameter, arg) + elif parameter == "time": + try: + info["time"] = int(tokens.pop(0)) / 1000.0 + except (ValueError, IndexError): + LOGGER.error("Exception parsing %s from info: %r", parameter, arg) + elif parameter == "ebf": + try: + info["ebf"] = float(tokens.pop(0)) + except (ValueError, IndexError): + 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 + if kind == "cp": + info["score"] = PovScore(Cp(int(value)), root_board.turn) + elif kind == "mate": + info["score"] = PovScore(Mate(int(value)), root_board.turn) + else: + LOGGER.error("Unknown score kind %r in info (expected cp or mate): %r", kind, arg) + except (ValueError, IndexError): + LOGGER.error("Exception parsing score from info: %r", arg) + elif parameter == "currmove": + try: + info["currmove"] = chess.Move.from_uci(tokens.pop(0)) + except (ValueError, IndexError): + LOGGER.error("Exception parsing currmove from info: %r", arg) + elif parameter == "currline" and selector & INFO_CURRLINE: + try: + if "currline" not in info: + info["currline"] = {} + + cpunr = int(tokens.pop(0)) + currline: List[chess.Move] = [] + info["currline"][cpunr] = currline + + board = root_board.copy(stack=False) + while tokens and UCI_REGEX.match(tokens[0]): + currline.append(board.push_uci(tokens.pop(0))) + except (ValueError, IndexError): + LOGGER.error("Exception parsing currline from info: %r, position at root: %s", arg, root_board.fen()) + elif parameter == "refutation" and selector & INFO_REFUTATION: + try: + if "refutation" not in info: + info["refutation"] = {} + + board = root_board.copy(stack=False) + refuted = board.push_uci(tokens.pop(0)) + + refuted_by: List[chess.Move] = [] + info["refutation"][refuted] = refuted_by + + while tokens and UCI_REGEX.match(tokens[0]): + refuted_by.append(board.push_uci(tokens.pop(0))) + except (ValueError, IndexError): + LOGGER.error("Exception parsing refutation from info: %r, position at root: %s", arg, root_board.fen()) + elif parameter == "pv" and selector & INFO_PV: + try: + pv: List[chess.Move] = [] + info["pv"] = pv + board = root_board.copy(stack=False) + while tokens and UCI_REGEX.match(tokens[0]): + pv.append(board.push_uci(tokens.pop(0))) + except (ValueError, IndexError): + LOGGER.error("Exception parsing pv from info: %r, position at root: %s", arg, root_board.fen()) + elif parameter == "wdl": + try: + info["wdl"] = PovWdl(Wdl(int(tokens.pop(0)), int(tokens.pop(0)), int(tokens.pop(0))), root_board.turn) + except (ValueError, IndexError): + LOGGER.error("Exception parsing wdl from info: %r", arg) + + return info + +def _parse_uci_bestmove(board: chess.Board, args: str) -> BestMove: + tokens = args.split() + + move = None + ponder = None + + if tokens and tokens[0] not in ["(none)", "NULL"]: + try: + # AnMon 5.75 uses uppercase letters to denote promotion types. + move = board.push_uci(tokens[0].lower()) + except ValueError as err: + raise EngineError(err) + + try: + # Houdini 1.5 sends NULL instead of skipping the token. + if len(tokens) >= 3 and tokens[1] == "ponder" and tokens[2] not in ["(none)", "NULL"]: + ponder = board.parse_uci(tokens[2].lower()) + except ValueError: + LOGGER.exception("Engine sent invalid ponder move") + finally: + board.pop() + + return BestMove(move, ponder) + + +class UciOptionMap(MutableMapping[str, T]): + """Dictionary with case-insensitive keys.""" + + def __init__(self, data: Optional[Union[Iterable[Tuple[str, T]]]] = None, **kwargs: T) -> None: + self._store: Dict[str, Tuple[str, T]] = {} + if data is None: + data = {} + self.update(data, **kwargs) + + def __setitem__(self, key: str, value: T) -> None: + self._store[key.lower()] = (key, value) + + def __getitem__(self, key: str) -> T: + return self._store[key.lower()][1] + + 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()) + + def __len__(self) -> int: + return len(self._store) + + def __eq__(self, other: object) -> bool: + try: + for key, value in self.items(): + if key not in other or other[key] != value: # type: ignore + return False + + for key, value in other.items(): # type: ignore + if key not in self or self[key] != value: + return False + + return True + except (TypeError, AttributeError): + return NotImplemented + + def copy(self) -> UciOptionMap[T]: + return type(self)(self._store.values()) + + def __copy__(self) -> UciOptionMap[T]: + return self.copy() + + def __repr__(self) -> str: + return f"{type(self).__name__}({dict(self.items())!r})" + + +XBOARD_ERROR_REGEX = re.compile(r"^\s*(Error|Illegal move)(\s*\([^()]+\))?\s*:") + + +class XBoardProtocol(Protocol): + """ + An implementation of the + `XBoard protocol `__ (CECP). + """ + + def __init__(self) -> None: + super().__init__() + self.features: Dict[str, Union[int, str]] = {} + self.id = {} + self.options = { + "random": Option("random", "check", False, None, None, None), + "computer": Option("computer", "check", False, None, None, None), + } + self.config: Dict[str, ConfigValue] = {} + self.target_config: Dict[str, ConfigValue] = {} + self.board = chess.Board() + self.game: object = None + self.first_game = True + + async def initialize(self) -> None: + class XBoardInitializeCommand(BaseCommand[XBoardProtocol, None]): + def check_initialized(self, engine: XBoardProtocol) -> None: + if 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)) + + def timeout(self, engine: XBoardProtocol) -> None: + LOGGER.error("%s: Timeout during initialization", engine) + self.end(engine) + + def line_received(self, engine: XBoardProtocol, line: str) -> None: + if line.startswith("#"): + pass + elif line.startswith("feature "): + self._feature(engine, line.split(" ", 1)[1]) + elif XBOARD_ERROR_REGEX.match(line): + raise EngineError(line) + + def _feature(self, engine: XBoardProtocol, 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 + else: + try: + engine.features[key] = int(value) + except ValueError: + engine.features[key] = value + + if "done" in engine.features: + self.timeout_handle.cancel() + if engine.features.get("done"): + self.end(engine) + + def end(self, engine: XBoardProtocol) -> None: + if not 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): + 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("rejected reuse") + if not engine.features.get("sigterm", 1): + LOGGER.warning("%s: Rejecting feature sigterm=0", engine) + engine.send_line("rejected sigterm") + if engine.features.get("san", 0): + LOGGER.warning("%s: Rejecting feature san=1", engine) + engine.send_line("rejected 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("accepted memory") + if engine.features.get("smp", 0): + engine.options["cores"] = Option("cores", "spin", 1, 1, None, None) + engine.send_line("accepted smp") + if engine.features.get("egt"): + for egt in str(engine.features["egt"]).split(","): + name = f"egtpath {egt}" + engine.options[name] = Option(name, "path", None, None, None, None) + engine.send_line("accepted egt") + + for option in engine.options.values(): + if option.default is not None: + engine.config[option.name] = option.default + if option.default is not None and not option.is_managed(): + engine.target_config[option.name] = option.default + + engine.initialized = True + self.result.set_result(None) + self.set_finished() + + return await self.communicate(XBoardInitializeCommand) + + def _ping(self, n: int) -> None: + self.send_line(f"ping {n}") + + def _variant(self, variant: Optional[str]) -> None: + variants = str(self.features.get("variants", "")).split(",") + if not variant or variant not in variants: + raise EngineError("unsupported xboard variant: {} (available: {})".format(variant, ", ".join(variants))) + + self.send_line(f"variant {variant}") + + def _new(self, board: chess.Board, game: object, options: ConfigMapping) -> None: + self._configure(options) + + # 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() + self.game = game + self.first_game = False + if new_game: + self.board = root + self.send_line("new") + + variant = type(board).xboard_variant + if variant == "normal" and board.chess960: + self._variant("fischerandom") + elif variant != "normal": + self._variant(variant) + + if self.config.get("random"): + self.send_line("random") + if self.config.get("computer"): + self.send_line("computer") + + 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}") + + # Undo moves until common position. + common_stack_len = 0 + if not new_game: + for left, right in zip(self.board.move_stack, board.move_stack): + if left == right: + common_stack_len += 1 + else: + break + + while len(self.board.move_stack) > common_stack_len + 1: + self.send_line("remove") + self.board.pop() + self.board.pop() + + while len(self.board.move_stack) > common_stack_len: + self.send_line("undo") + self.board.pop() + + # Play moves from board stack. + for move in board.move_stack[common_stack_len:]: + if not move: + LOGGER.warning("Null move (in %s) may not be supported by all XBoard engines", self.board.fen()) + prefix = "usermove " if self.features.get("usermove", 0) else "" + self.send_line(prefix + self.board.xboard(move)) + self.board.push(move) + + async def ping(self) -> None: + class XBoardPingCommand(BaseCommand[XBoardProtocol, None]): + def start(self, engine: XBoardProtocol) -> None: + n = id(self) & 0xffff + self.pong = f"pong {n}" + engine._ping(n) + + def line_received(self, engine: XBoardProtocol, line: str) -> None: + if line == self.pong: + self.result.set_result(None) + self.set_finished() + elif not line.startswith("#"): + LOGGER.warning("%s: Unexpected engine output: %r", engine, line) + elif XBOARD_ERROR_REGEX.match(line): + raise EngineError(line) + + return await self.communicate(XBoardPingCommand) + + async def play(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_NONE, ponder: bool = False, draw_offered: bool = False, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> 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: + 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) + + # 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 clock is not None or increment is not None: + base_mins, base_secs = divmod(int(clock or 0), 60) + engine.send_line(f"level {limit.remaining_moves or 0} {base_mins}:{base_secs:02d} {increment or 0}") + + 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"]: + raise EngineError("xboard engine does not support node limits (feature nps=0)") + + engine.send_line("nps 1") + engine.send_line(f"st {max(1, int(limit.nodes))}") + if limit.time is not None: + engine.send_line(f"st {max(0.01, limit.time)}") + if limit.depth is not None: + 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", max(1, int(limit.white_clock * 100)))) + if limit.black_clock is not None: + engine.send_line("{} {}".format("otim" if board.turn else "time", max(1, int(limit.black_clock * 100)))) + + if draw_offered and engine.features.get("draw", 1): + 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.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": + if not self.result.done(): + self.play_result.draw_offered = True + self._ping_after_move(engine) + elif line == "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("#"): + pass + elif XBOARD_ERROR_REGEX.match(line): + 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) + else: + LOGGER.warning("%s: Unexpected engine output: %r", engine, line) + + def _post(self, engine: XBoardProtocol, line: str) -> None: + if not self.result.done(): + self.play_result.info = _parse_xboard_post(line, engine.board, info) + + def _move(self, engine: XBoardProtocol, 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) + except ValueError as err: + self.result.set_exception(EngineError(err)) + else: + self._ping_after_move(engine) + else: + try: + engine.board.push_xboard(arg) + except ValueError: + LOGGER.exception("Exception playing unexpected move") + + def _hint(self, engine: XBoardProtocol, 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) + except ValueError: + LOGGER.exception("Exception parsing hint") + else: + LOGGER.warning("Unexpected hint: %r", arg) + + def _ping_after_move(self, engine: XBoardProtocol) -> None: + if self.pong_after_move is None: + n = id(self) & 0xffff + self.pong_after_move = f"pong {n}" + engine._ping(n) + + def cancel(self, engine: XBoardProtocol) -> None: + if self.stopped: + return + self.stopped = True + + if self.result.cancelled(): + engine.send_line("?") + + if ponder: + engine.send_line("easy") + + n = (id(self) + 1) & 0xffff + self.pong_after_ponder = f"pong {n}" + engine._ping(n) + + def engine_terminated(self, engine: XBoardProtocol, exc: Exception) -> None: + # Allow terminating engine while pondering. + if not self.result.done(): + super().engine_terminated(engine, exc) + + return await self.communicate(XBoardPlayCommand) + + async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> AnalysisResult: + if multipv is not None: + raise EngineError("xboard engine does not support multipv") + + if limit is not None and (limit.white_clock is not None or limit.black_clock is not None): + raise EngineError("xboard analysis does not support clock limits") + + class XBoardAnalysisCommand(BaseCommand[XBoardProtocol, AnalysisResult]): + def start(self, engine: XBoardProtocol) -> None: + self.stopped = False + self.best_move: Optional[chess.Move] = None + self.analysis = AnalysisResult(stop=lambda: self.cancel(engine)) + self.final_pong: Optional[str] = None + + engine._new(board, game, options) + + if root_moves is not None: + if not engine.features.get("exclude", 0): + raise EngineError("xboard engine does not support root_moves (feature exclude=0)") + + engine.send_line("exclude all") + for move in root_moves: + engine.send_line(f"include {engine.board.xboard(move)}") + + engine.send_line("post") + 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)) + else: + self.time_limit_handle = None + + def line_received(self, engine: XBoardProtocol, line: str) -> None: + if line.startswith("#"): + pass + elif len(line.split()) >= 4 and line.lstrip()[0].isdigit(): + self._post(engine, line) + elif line == self.final_pong: + self.end(engine) + elif XBOARD_ERROR_REGEX.match(line): + engine.first_game = True # Board state might no longer be in sync + raise EngineError(line) + else: + LOGGER.warning("%s: Unexpected engine output: %r", engine, line) + + def _post(self, engine: XBoardProtocol, line: str) -> None: + post_info = _parse_xboard_post(line, engine.board, info) + self.analysis.post(post_info) + + pv = post_info.get("pv") + if pv: + self.best_move = pv[0] + + if limit is not None: + if limit.time is not None and post_info.get("time", 0) >= limit.time: + self.cancel(engine) + elif limit.nodes is not None and post_info.get("nodes", 0) >= limit.nodes: + self.cancel(engine) + elif limit.depth is not None and post_info.get("depth", 0) >= limit.depth: + self.cancel(engine) + elif limit.mate is not None and "score" in post_info: + if post_info["score"].relative >= Mate(limit.mate): + self.cancel(engine) + + def end(self, engine: XBoardProtocol) -> None: + if self.time_limit_handle: + self.time_limit_handle.cancel() + + self.set_finished() + self.analysis.set_finished(BestMove(self.best_move, None)) + + def cancel(self, engine: XBoardProtocol) -> None: + if self.stopped: + return + self.stopped = True + + engine.send_line(".") + engine.send_line("exit") + + n = id(self) & 0xffff + self.final_pong = f"pong {n}" + 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) + + if self.time_limit_handle: + self.time_limit_handle.cancel() + + self.analysis.set_exception(exc) + + return await self.communicate(XBoardAnalysisCommand) + + def _setoption(self, name: str, value: ConfigValue) -> None: + if value is not None and value == self.config.get(name): + return + + try: + option = self.options[name] + except KeyError: + raise EngineError(f"unsupported xboard option or command: {name}") + + self.config[name] = value = option.parse(value) + + if name in ["random", "computer"]: + # Applied in _new. + pass + elif name in ["memory", "cores"] or name.startswith("egtpath "): + self.send_line(f"{name} {value}") + elif value is None: + self.send_line(f"option {name}") + elif value is True: + self.send_line(f"option {name}=1") + elif value is False: + self.send_line(f"option {name}=0") + else: + self.send_line(f"option {name}={value}") + + def _configure(self, options: ConfigMapping) -> None: + for name, value in collections.ChainMap(options, self.target_config).items(): + 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}) + self.result.set_result(None) + self.set_finished() + + return await self.communicate(XBoardConfigureCommand) + + async def quit(self) -> None: + self.send_line("quit") + await asyncio.shield(self.returncode) + + +def _parse_xboard_option(feature: str) -> Option: + params = feature.split() + + name = params[0] + type = params[1][1:] + default: Optional[ConfigValue] = None + min = None + max = None + var = None + + if type == "combo": + var = [] + choices = params[2:] + for choice in choices: + if choice == "///": + continue + elif choice[0] == "*": + default = choice[1:] + var.append(choice[1:]) + else: + var.append(choice) + elif type == "check": + default = int(params[2]) + elif type in ["string", "file", "path"]: + if len(params) > 2: + default = params[2] + else: + default = "" + elif type == "spin": + default = int(params[2]) + min = int(params[3]) + max = int(params[4]) + + return Option(name, type, default, min, max, var) + + +def _parse_xboard_post(line: str, root_board: chess.Board, selector: Info = INFO_ALL) -> InfoDict: + # Format: depth score time nodes [seldepth [nps [tbhits]]] pv + info: InfoDict = {} + + # Split leading integer tokens from pv. + pv_tokens = line.split() + integer_tokens = [] + while pv_tokens: + token = pv_tokens.pop(0) + try: + integer_tokens.append(int(token)) + except ValueError: + pv_tokens.insert(0, token) + break + + if len(integer_tokens) < 4: + return info + + # Required integer tokens. + info["depth"] = integer_tokens.pop(0) + cp = integer_tokens.pop(0) + info["time"] = int(integer_tokens.pop(0)) / 100 + info["nodes"] = int(integer_tokens.pop(0)) + + # Score. + if cp <= -100000: + score: Score = Mate(cp + 100000) + elif cp == 100000: + score = MateGiven + elif cp >= 100000: + score = Mate(cp - 100000) + else: + score = Cp(cp) + info["score"] = PovScore(score, root_board.turn) + + # Optional integer tokens. + if integer_tokens: + info["seldepth"] = integer_tokens.pop(0) + if integer_tokens: + info["nps"] = integer_tokens.pop(0) + + while len(integer_tokens) > 1: + # Reserved for future extensions. + integer_tokens.pop(0) + + if integer_tokens: + info["tbhits"] = integer_tokens.pop(0) + + # Principal variation. + pv = [] + board = root_board.copy(stack=False) + for token in pv_tokens: + if token.rstrip(".").isdigit(): + continue + + try: + pv.append(board.push_xboard(token)) + except ValueError: + break + + if not (selector & INFO_PV): + break + info["pv"] = pv + + return info + + +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.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[chess.engine.InfoDict] + """ + A list of dictionaries with aggregated information sent by the engine. + One item for each root move. + """ + + def __init__(self, stop: Optional[Callable[[], None]] = None): + self._stop = stop + self._queue: asyncio.Queue[InfoDict] = asyncio.Queue() + self._posted_kork = False + self._seen_kork = False + self._finished: asyncio.Future[BestMove] = asyncio.Future() + self.multipv = [{}] + + def post(self, info: InfoDict) -> None: + # Empty dictionary reserved for kork. + if not info: + return + + multipv = info.get("multipv", 1) + while len(self.multipv) < multipv: + self.multipv.append({}) + self.multipv[multipv - 1].update(info) + + self._queue.put_nowait(info) + + def _kork(self) -> None: + if not self._posted_kork: + self._posted_kork = True + self._queue.put_nowait({}) + + def set_finished(self, best: BestMove) -> None: + if not self._finished.done(): + self._finished.set_result(best) + self._kork() + + def set_exception(self, exc: Exception) -> None: + self._finished.set_exception(exc) + self._kork() + + @property + def info(self) -> InfoDict: + """ + A dictionary of aggregated information sent by the engine. This is + actually an alias for ``multipv[0]``. + """ + return self.multipv[0] + + def stop(self) -> None: + """Stops the analysis as soon as possible.""" + if self._stop and not self._posted_kork: + self._stop() + self._stop = None + + async def wait(self) -> BestMove: + """Waits until the analysis is complete (or stopped).""" + return await self._finished + + async def get(self) -> InfoDict: + """ + Waits for the next dictionary of information from the engine and + returns it. + + It might be more convenient to use ``async for info in analysis: ...``. + + :raises: :exc:`chess.engine.AnalysisComplete` if the analysis is + complete (or has been stopped) and all information has been + consumed. Use :func:`~chess.engine.AnalysisResult.next()` if you + prefer to get ``None`` instead of an exception. + """ + if self._seen_kork: + raise AnalysisComplete() + + info = await self._queue.get() + if not info: + # Empty dictionary marks end. + self._seen_kork = True + await self._finished + raise AnalysisComplete() + + return info + + def empty(self) -> bool: + """ + Checks if all 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 + + async def next(self) -> Optional[InfoDict]: + try: + return await self.get() + except AnalysisComplete: + return None + + def __aiter__(self) -> AnalysisResult: + return self + + async def __anext__(self) -> InfoDict: + try: + return await self.get() + except AnalysisComplete: + raise StopAsyncIteration + + def __enter__(self) -> AnalysisResult: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.stop() + + +async def popen_uci(command: Union[str, List[str]], *, setpgrp: bool = False, **popen_args: Any) -> Tuple[asyncio.SubprocessTransport, UciProtocol]: + """ + Spawns and initializes a UCI engine. + + :param command: Path of the engine executable, or a list including the + path and arguments. + :param setpgrp: Open the engine process in a new process group. This will + stop signals (such as keyboard interrupts) from propagating from the + parent process. Defaults to ``False``. + :param popen_args: Additional arguments for + `popen `_. + Do not set ``stdin``, ``stdout``, ``bufsize`` or + ``universal_newlines``. + + Returns a subprocess transport and engine protocol pair. + """ + transport, protocol = await UciProtocol.popen(command, setpgrp=setpgrp, **popen_args) + try: + await protocol.initialize() + except: + transport.close() + raise + return transport, protocol + + +async def popen_xboard(command: Union[str, List[str]], *, setpgrp: bool = False, **popen_args: Any) -> Tuple[asyncio.SubprocessTransport, XBoardProtocol]: + """ + Spawns and initializes an XBoard engine. + + :param command: Path of the engine executable, or a list including the + path and arguments. + :param setpgrp: Open the engine process in a new process group. This will + stop signals (such as keyboard interrupts) from propagating from the + parent process. Defaults to ``False``. + :param popen_args: Additional arguments for + `popen `_. + Do not set ``stdin``, ``stdout``, ``bufsize`` or + ``universal_newlines``. + + Returns a subprocess transport and engine protocol pair. + """ + transport, protocol = await XBoardProtocol.popen(command, setpgrp=setpgrp, **popen_args) + try: + await protocol.initialize() + except: + transport.close() + raise + return transport, protocol + + +class SimpleEngine: + """ + Synchronous wrapper around a transport and engine protocol pair. Provides + the same methods and attributes as :class:`chess.engine.Protocol` + with blocking functions instead of coroutines. + + You may not concurrently modify objects passed to any of the methods. Other + than that, :class:`~chess.engine.SimpleEngine` is thread-safe. When sending + a new command to the engine, any previous running command will be cancelled + as soon as possible. + + Methods will raise :class:`asyncio.TimeoutError` if an operation takes + *timeout* seconds longer than expected (unless *timeout* is ``None``). + + Automatically closes the transport when used as a context manager. + """ + + def __init__(self, transport: asyncio.SubprocessTransport, protocol: Protocol, *, timeout: Optional[float] = 10.0) -> None: + self.transport = transport + self.protocol = protocol + self.timeout = timeout + + self._shutdown_lock = threading.Lock() + self._shutdown = False + self.shutdown_event = asyncio.Event() + + self.returncode: concurrent.futures.Future[int] = concurrent.futures.Future() + + def _timeout_for(self, limit: Optional[Limit]) -> Optional[float]: + if self.timeout is None or limit is None or limit.time is None: + return None + return self.timeout + limit.time + + @contextlib.contextmanager + def _not_shut_down(self) -> Generator[None, None, None]: + with self._shutdown_lock: + if self._shutdown: + raise EngineTerminatedError("engine event loop dead") + yield + + @property + def options(self) -> MutableMapping[str, Option]: + 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) + 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) + return future.result() + + def communicate(self, command_factory: Callable[[Protocol], BaseCommand[Protocol, T]]) -> T: + with self._not_shut_down(): + coro = self.protocol.communicate(command_factory) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def configure(self, options: ConfigMapping) -> None: + with self._not_shut_down(): + coro = asyncio.wait_for(self.protocol.configure(options), self.timeout) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def ping(self) -> None: + with self._not_shut_down(): + coro = asyncio.wait_for(self.protocol.ping(), self.timeout) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def play(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_NONE, ponder: bool = False, draw_offered: bool = False, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> PlayResult: + with self._not_shut_down(): + coro = asyncio.wait_for( + self.protocol.play(board, limit, game=game, info=info, ponder=ponder, draw_offered=draw_offered, root_moves=root_moves, options=options), + self._timeout_for(limit)) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + @typing.overload + def analyse(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> InfoDict: ... + @typing.overload + def analyse(self, board: chess.Board, limit: Limit, *, multipv: int, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> List[InfoDict]: ... + @typing.overload + def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> Union[InfoDict, List[InfoDict]]: ... + def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> Union[InfoDict, List[InfoDict]]: + with self._not_shut_down(): + coro = asyncio.wait_for( + self.protocol.analyse(board, limit, multipv=multipv, game=game, info=info, root_moves=root_moves, options=options), + self._timeout_for(limit)) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> SimpleAnalysisResult: + with self._not_shut_down(): + coro = asyncio.wait_for( + self.protocol.analysis(board, limit, multipv=multipv, game=game, info=info, root_moves=root_moves, options=options), + self.timeout) # Timeout until analysis is *started* + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return SimpleAnalysisResult(self, future.result()) + + def quit(self) -> None: + with self._not_shut_down(): + coro = asyncio.wait_for(self.protocol.quit(), self.timeout) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def close(self) -> None: + """ + Closes the transport and the background event loop as soon as possible. + """ + def _shutdown() -> None: + self.transport.close() + self.shutdown_event.set() + + with self._shutdown_lock: + if not self._shutdown: + self._shutdown = True + self.protocol.loop.call_soon_threadsafe(_shutdown) + + @classmethod + def popen(cls, Protocol: Type[Protocol], command: Union[str, List[str]], *, timeout: Optional[float] = 10.0, debug: bool = False, setpgrp: bool = False, **popen_args: Any) -> SimpleEngine: + async def background(future: concurrent.futures.Future[SimpleEngine]) -> None: + transport, protocol = await Protocol.popen(command, setpgrp=setpgrp, **popen_args) + threading.current_thread().name = f"{cls.__name__} (pid={transport.get_pid()})" + simple_engine = cls(transport, protocol, timeout=timeout) + try: + await asyncio.wait_for(protocol.initialize(), timeout) + future.set_result(simple_engine) + returncode = await protocol.returncode + simple_engine.returncode.set_result(returncode) + finally: + simple_engine.close() + await simple_engine.shutdown_event.wait() + + return run_in_background(background, name=f"{cls.__name__} (command={command!r})", debug=debug) + + @classmethod + def popen_uci(cls, command: Union[str, List[str]], *, timeout: Optional[float] = 10.0, debug: bool = False, setpgrp: bool = False, **popen_args: Any) -> SimpleEngine: + """ + Spawns and initializes a UCI engine. + Returns a :class:`~chess.engine.SimpleEngine` instance. + """ + return cls.popen(UciProtocol, command, timeout=timeout, debug=debug, setpgrp=setpgrp, **popen_args) + + @classmethod + def popen_xboard(cls, command: Union[str, List[str]], *, timeout: Optional[float] = 10.0, debug: bool = False, setpgrp: bool = False, **popen_args: Any) -> SimpleEngine: + """ + Spawns and initializes an XBoard engine. + Returns a :class:`~chess.engine.SimpleEngine` instance. + """ + return cls.popen(XBoardProtocol, command, timeout=timeout, debug=debug, setpgrp=setpgrp, **popen_args) + + def __enter__(self) -> SimpleEngine: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.close() + + def __repr__(self) -> str: + pid = self.transport.get_pid() # This happens to be thread-safe + return f"<{type(self).__name__} (pid={pid})>" + + +class SimpleAnalysisResult: + """ + Synchronous wrapper around :class:`~chess.engine.AnalysisResult`. Returned + by :func:`chess.engine.SimpleEngine.analysis()`. + """ + + def __init__(self, simple_engine: SimpleEngine, inner: AnalysisResult) -> None: + self.simple_engine = simple_engine + self.inner = inner + + @property + def info(self) -> InfoDict: + 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) + 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) + return future.result() + + def stop(self) -> None: + with self.simple_engine._not_shut_down(): + self.simple_engine.protocol.loop.call_soon_threadsafe(self.inner.stop) + + def wait(self) -> BestMove: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(self.inner.wait(), self.simple_engine.protocol.loop) + return future.result() + + def empty(self) -> bool: + async def _empty() -> bool: + return self.inner.empty() + + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(_empty(), self.simple_engine.protocol.loop) + return future.result() + + def get(self) -> InfoDict: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(self.inner.get(), self.simple_engine.protocol.loop) + return future.result() + + def next(self) -> Optional[InfoDict]: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(self.inner.next(), self.simple_engine.protocol.loop) + return future.result() + + def __iter__(self) -> Iterator[InfoDict]: + with self.simple_engine._not_shut_down(): + self.simple_engine.protocol.loop.call_soon_threadsafe(self.inner.__aiter__) + return self + + def __next__(self) -> InfoDict: + try: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(self.inner.__anext__(), self.simple_engine.protocol.loop) + return future.result() + except StopAsyncIteration: + raise StopIteration + + def __enter__(self) -> SimpleAnalysisResult: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.stop() diff --git a/cartesi-python-chess-cartesi-img/chess/engine.pyc b/cartesi-python-chess-cartesi-img/chess/engine.pyc new file mode 100644 index 0000000..d83b598 Binary files /dev/null and b/cartesi-python-chess-cartesi-img/chess/engine.pyc differ diff --git a/cartesi-python-chess-cartesi-img/chess/gaviota.py b/cartesi-python-chess-cartesi-img/chess/gaviota.py new file mode 100644 index 0000000..4549497 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/gaviota.py @@ -0,0 +1,2111 @@ +# This file is part of the python-chess library. +# Copyright (C) 2015 Jean-Noël Avila +# Copyright (C) 2015-2021 Niklas Fiekas +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import annotations + +import ctypes +import ctypes.util +import dataclasses +import fnmatch +import logging +import lzma +import os +import os.path +import struct +import typing + +import chess + +from types import TracebackType +from typing import BinaryIO, Callable, Dict, List, Optional, Tuple, Type, Union + + +LOGGER = logging.getLogger(__name__) + + +NOSQUARE = 64 +NOINDEX = -1 + +MAX_KKINDEX = 462 +MAX_PPINDEX = 576 +MAX_PpINDEX = 24 * 48 +MAX_AAINDEX = (63 - 62) + (62 // 2 * (127 - 62)) - 1 + 1 +MAX_AAAINDEX = 64 * 21 * 31 +MAX_PPP48_INDEX = 8648 +MAX_PP48_INDEX = 1128 + +MAX_KXK = MAX_KKINDEX * 64 +MAX_kabk = MAX_KKINDEX * 64 * 64 +MAX_kakb = MAX_KKINDEX * 64 * 64 +MAX_kpk = 24 * 64 * 64 +MAX_kakp = 24 * 64 * 64 * 64 +MAX_kapk = 24 * 64 * 64 * 64 +MAX_kppk = MAX_PPINDEX * 64 * 64 +MAX_kpkp = MAX_PpINDEX * 64 * 64 +MAX_kaak = MAX_KKINDEX * MAX_AAINDEX +MAX_kabkc = MAX_KKINDEX * 64 * 64 * 64 +MAX_kabck = MAX_KKINDEX * 64 * 64 * 64 +MAX_kaakb = MAX_KKINDEX * MAX_AAINDEX * 64 +MAX_kaabk = MAX_KKINDEX * MAX_AAINDEX * 64 +MAX_kabbk = MAX_KKINDEX * MAX_AAINDEX * 64 +MAX_kaaak = MAX_KKINDEX * MAX_AAAINDEX +MAX_kapkb = 24 * 64 * 64 * 64 * 64 +MAX_kabkp = 24 * 64 * 64 * 64 * 64 +MAX_kabpk = 24 * 64 * 64 * 64 * 64 +MAX_kppka = MAX_kppk * 64 +MAX_kappk = MAX_kppk * 64 +MAX_kapkp = MAX_kpkp * 64 +MAX_kaapk = 24 * MAX_AAINDEX * 64 * 64 +MAX_kaakp = 24 * MAX_AAINDEX * 64 * 64 +MAX_kppkp = 24 * MAX_PP48_INDEX * 64 * 64 +MAX_kpppk = MAX_PPP48_INDEX * 64 * 64 + +PLYSHIFT = 3 +INFOMASK = 7 + +WE_FLAG = 1 +NS_FLAG = 2 +NW_SE_FLAG = 4 + +ITOSQ = [ + chess.H7, chess.G7, chess.F7, chess.E7, + chess.H6, chess.G6, chess.F6, chess.E6, + chess.H5, chess.G5, chess.F5, chess.E5, + chess.H4, chess.G4, chess.F4, chess.E4, + chess.H3, chess.G3, chess.F3, chess.E3, + chess.H2, chess.G2, chess.F2, chess.E2, + chess.D7, chess.C7, chess.B7, chess.A7, + chess.D6, chess.C6, chess.B6, chess.A6, + chess.D5, chess.C5, chess.B5, chess.A5, + chess.D4, chess.C4, chess.B4, chess.A4, + chess.D3, chess.C3, chess.B3, chess.A3, + chess.D2, chess.C2, chess.B2, chess.A2, +] + +ENTRIES_PER_BLOCK = 16 * 1024 + +EGTB_MAXBLOCKSIZE = 65536 + + +def map24_b(s: int) -> int: + s -= 8 + return ((s & 3) + s) >> 1 + +def map88(x: int) -> int: + return x + (x & 56) + +def in_queenside(x: int) -> int: + return (x & (1 << 2)) == 0 + +def flip_we(x: int) -> int: + return x ^ 7 + +def flip_ns(x: int) -> int: + return x ^ 56 + +def flip_nw_se(x: int) -> int: + return ((x & 7) << 3) | (x >> 3) + +def idx_is_empty(x: int) -> int: + return x == -1 + + +def flip_type(x: chess.Square, y: chess.Square) -> int: + ret = 0 + + if chess.square_file(x) > 3: + x = flip_we(x) + y = flip_we(y) + ret |= 1 + + if chess.square_rank(x) > 3: + x = flip_ns(x) + y = flip_ns(y) + ret |= 2 + + rowx = chess.square_rank(x) + colx = chess.square_file(x) + + if rowx > colx: + x = flip_nw_se(x) + y = flip_nw_se(y) + ret |= 4 + + rowy = chess.square_rank(y) + coly = chess.square_file(y) + if rowx == colx and rowy > coly: + x = flip_nw_se(x) + y = flip_nw_se(y) + ret |= 4 + + return ret + +def init_flipt() -> List[List[int]]: + return [[flip_type(j, i) for i in range(64)] for j in range(64)] + +FLIPT = init_flipt() + + +def init_pp48_idx() -> Tuple[List[List[int]], List[int], List[int]]: + MAX_I = 48 + MAX_J = 48 + idx = 0 + pp48_idx = [[-1] * MAX_J for i in range(MAX_I)] + pp48_sq_x = [NOSQUARE] * MAX_PP48_INDEX + pp48_sq_y = [NOSQUARE] * MAX_PP48_INDEX + + idx = 0 + for a in range(chess.H7, chess.A2 - 1, -1): + for b in range(a - 1, chess.A2 - 1, -1): + i = flip_we(flip_ns(a)) - 8 + j = flip_we(flip_ns(b)) - 8 + + if idx_is_empty(pp48_idx[i][j]): + pp48_idx[i][j] = idx + pp48_idx[j][i] = idx + pp48_sq_x[idx] = i + pp48_sq_y[idx] = j + idx += 1 + + return pp48_idx, pp48_sq_x, pp48_sq_y + +PP48_IDX, PP48_SQ_X, PP48_SQ_Y = init_pp48_idx() + + +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)] + ppp48_sq_x = [NOSQUARE] * MAX_PPP48_INDEX + ppp48_sq_y = [NOSQUARE] * MAX_PPP48_INDEX + ppp48_sq_z = [NOSQUARE] * MAX_PPP48_INDEX + + idx = 0 + for x in range(48): + for y in range(x + 1, 48): + for z in range(y + 1, 48): + a = ITOSQ[x] + b = ITOSQ[y] + c = ITOSQ[z] + if not in_queenside(b) or not in_queenside(c): + continue + + i = a - 8 + j = b - 8 + k = c - 8 + + if idx_is_empty(ppp48_idx[i][j][k]): + ppp48_idx[i][j][k] = idx + ppp48_idx[i][k][j] = idx + ppp48_idx[j][i][k] = idx + ppp48_idx[j][k][i] = idx + ppp48_idx[k][i][j] = idx + ppp48_idx[k][j][i] = idx + ppp48_sq_x[idx] = i + ppp48_sq_y[idx] = j + ppp48_sq_z[idx] = k + idx += 1 + + return ppp48_idx, ppp48_sq_x, ppp48_sq_y, ppp48_sq_z + +PPP48_IDX, PPP48_SQ_X, PPP48_SQ_Y, PPP48_SQ_Z = init_ppp48_idx() + + +def init_aaidx() -> Tuple[List[int], List[List[int]]]: + aaidx = [[-1] * 64 for y in range(64)] + aabase = [0] * MAX_AAINDEX + + idx = 0 + for x in range(64): + for y in range(x + 1, 64): + + if idx_is_empty(aaidx[x][y]): + # Still empty. + aaidx[x][y] = idx + aaidx[y][x] = idx + aabase[idx] = x + idx += 1 + + return aabase, aaidx + +AABASE, AAIDX = init_aaidx() + + +def init_aaa() -> Tuple[List[int], List[List[int]]]: + # Get aaa_base. + comb = [a * (a - 1) // 2 for a in range(64)] + + accum = 0 + aaa_base = [0] * 64 + for a in range(64 - 1): + accum += comb[a] + aaa_base[a + 1] = accum + + # Get aaa_xyz. + aaa_xyz = [[-1] * 3 for idx in range(MAX_AAAINDEX)] + + idx = 0 + for z in range(64): + for y in range(z): + for x in range(y): + aaa_xyz[idx][0] = x + aaa_xyz[idx][1] = y + aaa_xyz[idx][2] = z + idx += 1 + + return aaa_base, aaa_xyz + +AAA_BASE, AAA_XYZ = init_aaa() + + +def pp_putanchorfirst(a: int, b: int) -> Tuple[int, int]: + row_b = b & 56 + row_a = a & 56 + + # Default. + anchor = a + loosen = b + + if row_b > row_a: + anchor = b + loosen = a + elif row_b == row_a: + x = a + col = x & 7 + inv = col ^ 7 + x = (1 << col) | (1 << inv) + x &= (x - 1) + hi_a = x + + x = b + col = x & 7 + inv = col ^ 7 + x = (1 << col) | (1 << inv) + x &= (x - 1) + hi_b = x + + if hi_b > hi_a: + anchor = b + loosen = a + + if hi_b < hi_a: + anchor = a + loosen = b + + if hi_b == hi_a: + if a < b: + anchor = a + loosen = b + else: + anchor = b + loosen = a + + return anchor, loosen + +def wsq_to_pidx24(pawn: int) -> int: + sq = pawn + + sq = flip_ns(sq) + sq -= 8 # Down one row + + idx24 = (sq + (sq & 3)) >> 1 + return idx24 + +def wsq_to_pidx48(pawn: int) -> int: + sq = pawn + + sq = flip_ns(sq) + 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)] + pp_hi24 = [-1] * MAX_PPINDEX + pp_lo48 = [-1] * MAX_PPINDEX + + idx = 0 + for a in range(chess.H7, chess.A2 - 1, -1): + if in_queenside(a): + continue + + for b in range(a - 1, chess.A2 - 1, -1): + anchor = 0 + loosen = 0 + + anchor, loosen = pp_putanchorfirst(a, b) + + if (anchor & 7) > 3: + # Square on the kingside. + anchor = flip_we(anchor) + loosen = flip_we(loosen) + + i = wsq_to_pidx24(anchor) + j = wsq_to_pidx48(loosen) + + if idx_is_empty(ppidx[i][j]): + ppidx[i][j] = idx + pp_hi24[idx] = i + pp_lo48[idx] = j + idx += 1 + + return ppidx, pp_hi24, pp_lo48 + +PPIDX, PP_HI24, PP_LO48 = init_ppidx() + + +def norm_kkindex(x: chess.Square, y: chess.Square) -> Tuple[int, int]: + if chess.square_file(x) > 3: + x = flip_we(x) + y = flip_we(y) + + if chess.square_rank(x) > 3: + x = flip_ns(x) + y = flip_ns(y) + + rowx = chess.square_rank(x) + colx = chess.square_file(x) + + if rowx > colx: + x = flip_nw_se(x) + y = flip_nw_se(y) + + rowy = chess.square_rank(y) + coly = chess.square_file(y) + + if rowx == colx and rowy > coly: + x = flip_nw_se(x) + y = flip_nw_se(y) + + return x, y + +def init_kkidx() -> Tuple[List[List[int]], List[int], List[int]]: + kkidx = [[-1] * 64 for x in range(64)] + bksq = [-1] * MAX_KKINDEX + wksq = [-1] * MAX_KKINDEX + idx = 0 + for x in range(64): + for y in range(64): + # Check if x to y is legal. + if x != y and not chess.BB_KING_ATTACKS[x] & chess.BB_SQUARES[y]: + # Normalize. + i, j = norm_kkindex(x, y) + + if idx_is_empty(kkidx[i][j]): + kkidx[i][j] = idx + kkidx[x][y] = idx + bksq[idx] = i + wksq[idx] = j + idx += 1 + + return kkidx, wksq, bksq + +KKIDX, WKSQ, BKSQ = init_kkidx() + + +def kxk_pctoindex(c: Request) -> int: + BLOCK_Ax = 64 + + ft = flip_type(c.black_piece_squares[0], c.white_piece_squares[0]) + + ws = c.white_piece_squares + bs = c.black_piece_squares + + if (ft & 1) != 0: + ws = [flip_we(b) for b in ws] + bs = [flip_we(b) for b in bs] + + if (ft & 2) != 0: + ws = [flip_ns(b) for b in ws] + bs = [flip_ns(b) for b in bs] + + if (ft & 4) != 0: + ws = [flip_nw_se(b) for b in ws] + bs = [flip_nw_se(b) for b in bs] + + ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] + + if ki == -1: + return NOINDEX + + return ki * BLOCK_Ax + ws[1] + +def kapkb_pctoindex(c: Request) -> int: + BLOCK_A = 64 * 64 * 64 * 64 + BLOCK_B = 64 * 64 * 64 + BLOCK_C = 64 * 64 + BLOCK_D = 64 + + pawn = c.white_piece_squares[2] + wa = c.white_piece_squares[1] + wk = c.white_piece_squares[0] + bk = c.black_piece_squares[0] + ba = c.black_piece_squares[1] + + if not (chess.A2 <= pawn < chess.A8): + return NOINDEX + + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + ba = flip_we(ba) + + sq = pawn + sq ^= 56 # flip_ns + 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: + BLOCK_A = 64 * 64 * 64 * 64 + BLOCK_B = 64 * 64 * 64 + BLOCK_C = 64 * 64 + BLOCK_D = 64 + + wk = c.white_piece_squares[0] + wa = c.white_piece_squares[1] + wb = c.white_piece_squares[2] + pawn = c.white_piece_squares[3] + bk = c.black_piece_squares[0] + + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + wb = flip_we(wb) + + pslice = wsq_to_pidx24(pawn) + + return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa * BLOCK_D + wb + +def kabkp_pctoindex(c: Request) -> int: + BLOCK_A = 64 * 64 * 64 * 64 + BLOCK_B = 64 * 64 * 64 + BLOCK_C = 64 * 64 + BLOCK_D = 64 + + pawn = c.black_piece_squares[1] + wa = c.white_piece_squares[1] + wk = c.white_piece_squares[0] + bk = c.black_piece_squares[0] + wb = c.white_piece_squares[2] + + if not (chess.A2 <= pawn < chess.A8): + return NOINDEX + + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + wb = flip_we(wb) + + sq = pawn + 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: + BLOCK_C = MAX_AAINDEX + BLOCK_B = 64 * BLOCK_C + BLOCK_A = 64 * BLOCK_B + + wk = c.white_piece_squares[0] + wa = c.white_piece_squares[1] + wa2 = c.white_piece_squares[2] + pawn = c.white_piece_squares[3] + bk = c.black_piece_squares[0] + + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + wa2 = flip_we(wa2) + + pslice = wsq_to_pidx24(pawn) + + aa_combo = AAIDX[wa][wa2] + + if idx_is_empty(aa_combo): + return NOINDEX + + return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + aa_combo + +def kaakp_pctoindex(c: Request) -> int: + BLOCK_C = MAX_AAINDEX + BLOCK_B = 64 * BLOCK_C + BLOCK_A = 64 * BLOCK_B + + wk = c.white_piece_squares[0] + wa = c.white_piece_squares[1] + wa2 = c.white_piece_squares[2] + bk = c.black_piece_squares[0] + pawn = c.black_piece_squares[1] + + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + wa2 = flip_we(wa2) + + pawn = flip_ns(pawn) + pslice = wsq_to_pidx24(pawn) + + aa_combo = AAIDX[wa][wa2] + + if idx_is_empty(aa_combo): + return NOINDEX + + return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + aa_combo + +def kapkp_pctoindex(c: Request) -> int: + BLOCK_A = 64 * 64 * 64 + BLOCK_B = 64 * 64 + BLOCK_C = 64 + + wk = c.white_piece_squares[0] + wa = c.white_piece_squares[1] + pawn_a = c.white_piece_squares[2] + bk = c.black_piece_squares[0] + pawn_b = c.black_piece_squares[1] + + anchor = pawn_a + loosen = pawn_b + + if (anchor & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + + m = wsq_to_pidx24(anchor) + n = loosen - 8 + pp_slice = m * 48 + n + + if idx_is_empty(pp_slice): + return NOINDEX + + return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa + +def kappk_pctoindex(c: Request) -> int: + BLOCK_A = 64 * 64 * 64 + BLOCK_B = 64 * 64 + BLOCK_C = 64 + + wk = c.white_piece_squares[0] + wa = c.white_piece_squares[1] + pawn_a = c.white_piece_squares[2] + pawn_b = c.white_piece_squares[3] + bk = c.black_piece_squares[0] + + anchor, loosen = pp_putanchorfirst(pawn_a, pawn_b) + + if (anchor & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + + i = wsq_to_pidx24(anchor) + j = wsq_to_pidx48(loosen) + + pp_slice = PPIDX[i][j] + + if idx_is_empty(pp_slice): + return NOINDEX + + return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa + +def kppka_pctoindex(c: Request) -> int: + BLOCK_A = 64 * 64 * 64 + BLOCK_B = 64 * 64 + BLOCK_C = 64 + + wk = c.white_piece_squares[0] + pawn_a = c.white_piece_squares[1] + pawn_b = c.white_piece_squares[2] + bk = c.black_piece_squares[0] + ba = c.black_piece_squares[1] + + anchor, loosen = pp_putanchorfirst(pawn_a, pawn_b) + + if (anchor & 7) > 3: + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) + ba = flip_we(ba) + + i = wsq_to_pidx24(anchor) + j = wsq_to_pidx48(loosen) + + pp_slice = PPIDX[i][j] + + if idx_is_empty(pp_slice): + return NOINDEX + + return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + ba + +def kabck_pctoindex(c: Request) -> int: + N_WHITE = 4 + N_BLACK = 1 + BLOCK_A = 64 * 64 * 64 + BLOCK_B = 64 * 64 + BLOCK_C = 64 + + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] + + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] + + ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] + + if idx_is_empty(ki): + return NOINDEX + + return ki * BLOCK_A + ws[1] * BLOCK_B + ws[2] * BLOCK_C + ws[3] + +def kabbk_pctoindex(c: Request) -> int: + N_WHITE = 4 + N_BLACK = 1 + BLOCK_Bx = 64 + BLOCK_Ax = BLOCK_Bx * MAX_AAINDEX + + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] + + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] + + ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] + ai = AAIDX[ws[2]][ws[3]] + + if idx_is_empty(ki) or idx_is_empty(ai): + return NOINDEX + + return ki * BLOCK_Ax + ai * BLOCK_Bx + ws[1] + +def kaabk_pctoindex(c: Request) -> int: + N_WHITE = 4 + N_BLACK = 1 + BLOCK_Bx = 64 + BLOCK_Ax = BLOCK_Bx * MAX_AAINDEX + + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] + + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] + + ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] + ai = AAIDX[ws[1]][ws[2]] + + if idx_is_empty(ki) or idx_is_empty(ai): + return NOINDEX + + return ki * BLOCK_Ax + ai * BLOCK_Bx + ws[3] + +def aaa_getsubi(x: int, y: int, z: int) -> int: + bse = AAA_BASE[z] + calc_idx = x + (y - 1) * y // 2 + bse + return calc_idx + +def kaaak_pctoindex(c: Request) -> int: + N_WHITE = 4 + N_BLACK = 1 + BLOCK_Ax = MAX_AAAINDEX + + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] + + if ws[2] < ws[1]: + tmp = ws[1] + ws[1] = ws[2] + ws[2] = tmp + if ws[3] < ws[2]: + tmp = ws[2] + ws[2] = ws[3] + ws[3] = tmp + if ws[2] < ws[1]: + tmp = ws[1] + ws[1] = ws[2] + ws[2] = tmp + + ki = KKIDX[bs[0]][ws[0]] + + if ws[1] == ws[2] or ws[1] == ws[3] or ws[2] == ws[3]: + return NOINDEX + + ai = aaa_getsubi(ws[1], ws[2], ws[3]) + + if idx_is_empty(ki) or idx_is_empty(ai): + return NOINDEX + + return ki * BLOCK_Ax + ai + +def kppkp_pctoindex(c: Request) -> int: + BLOCK_Ax = MAX_PP48_INDEX * 64 * 64 + BLOCK_Bx = 64 * 64 + BLOCK_Cx = 64 + + wk = c.white_piece_squares[0] + pawn_a = c.white_piece_squares[1] + pawn_b = c.white_piece_squares[2] + bk = c.black_piece_squares[0] + pawn_c = c.black_piece_squares[1] + + if (pawn_c & 7) > 3: + wk = flip_we(wk) + pawn_a = flip_we(pawn_a) + pawn_b = flip_we(pawn_b) + bk = flip_we(bk) + pawn_c = flip_we(pawn_c) + + i = flip_we(flip_ns(pawn_a)) - 8 + j = flip_we(flip_ns(pawn_b)) - 8 + + # Black pawn, so low indexes are more advanced. + k = map24_b(pawn_c) + + pp48_slice = PP48_IDX[i][j] + + if idx_is_empty(pp48_slice): + return NOINDEX + + return k * BLOCK_Ax + pp48_slice * BLOCK_Bx + wk * BLOCK_Cx + bk + +def kaakb_pctoindex(c: Request) -> int: + N_WHITE = 3 + N_BLACK = 2 + BLOCK_Bx = 64 + BLOCK_Ax = BLOCK_Bx * MAX_AAINDEX + + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] + + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] + + ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] + ai = AAIDX[ws[1]][ws[2]] + + if idx_is_empty(ki) or idx_is_empty(ai): + return NOINDEX + + return ki * BLOCK_Ax + ai * BLOCK_Bx + bs[1] + +def kabkc_pctoindex(c: Request) -> int: + N_WHITE = 3 + N_BLACK = 2 + + BLOCK_Ax = 64 * 64 * 64 + BLOCK_Bx = 64 * 64 + BLOCK_Cx = 64 + + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] + + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] + + ki = KKIDX[bs[0]][ws[0]] # KKIDX [black king] [white king] + + if idx_is_empty(ki): + return NOINDEX + + return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + ws[2] * BLOCK_Cx + bs[1] + +def kpkp_pctoindex(c: Request) -> int: + BLOCK_Ax = 64 * 64 + BLOCK_Bx = 64 + + wk = c.white_piece_squares[0] + bk = c.black_piece_squares[0] + pawn_a = c.white_piece_squares[1] + pawn_b = c.black_piece_squares[1] + + anchor = pawn_a + loosen = pawn_b + + if (anchor & 7) > 3: + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) + + m = wsq_to_pidx24(anchor) + n = loosen - 8 + + pp_slice = m * 48 + n + + if idx_is_empty(pp_slice): + return NOINDEX + + return pp_slice * BLOCK_Ax + wk * BLOCK_Bx + bk + +def kppk_pctoindex(c: Request) -> int: + BLOCK_Ax = 64 * 64 + BLOCK_Bx = 64 + wk = c.white_piece_squares[0] + pawn_a = c.white_piece_squares[1] + pawn_b = c.white_piece_squares[2] + bk = c.black_piece_squares[0] + + anchor, loosen = pp_putanchorfirst(pawn_a, pawn_b) + + if (anchor & 7) > 3: + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) + + i = wsq_to_pidx24(anchor) + j = wsq_to_pidx48(loosen) + + pp_slice = PPIDX[i][j] + + if idx_is_empty(pp_slice): + return NOINDEX + + return pp_slice * BLOCK_Ax + wk * BLOCK_Bx + bk + +def kapk_pctoindex(c: Request) -> int: + BLOCK_Ax = 64 * 64 * 64 + BLOCK_Bx = 64 * 64 + BLOCK_Cx = 64 + + pawn = c.white_piece_squares[2] + wa = c.white_piece_squares[1] + wk = c.white_piece_squares[0] + bk = c.black_piece_squares[0] + + if not (chess.A2 <= pawn < chess.A8): + return NOINDEX + + if (pawn & 7) > 3: + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + + sq = pawn + sq ^= 56 # flip_ns + 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: + BLOCK_Ax = 64 * 64 + BLOCK_Bx = 64 + + ft = flip_type(c.black_piece_squares[0], c.white_piece_squares[0]) + + ws = c.white_piece_squares + bs = c.black_piece_squares + + if (ft & 1) != 0: + ws = [flip_we(b) for b in ws] + bs = [flip_we(b) for b in bs] + + if (ft & 2) != 0: + ws = [flip_ns(b) for b in ws] + bs = [flip_ns(b) for b in bs] + + if (ft & 4) != 0: + ws = [flip_nw_se(b) for b in ws] + bs = [flip_nw_se(b) for b in bs] + + ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] + + if idx_is_empty(ki): + return NOINDEX + + return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + ws[2] + +def kakp_pctoindex(c: Request) -> int: + BLOCK_Ax = 64 * 64 * 64 + BLOCK_Bx = 64 * 64 + BLOCK_Cx = 64 + + pawn = c.black_piece_squares[1] + wa = c.white_piece_squares[1] + wk = c.white_piece_squares[0] + bk = c.black_piece_squares[0] + + if not (chess.A2 <= pawn < chess.A8): + return NOINDEX + + if (pawn & 7) > 3: + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + + sq = pawn + 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: + N_WHITE = 3 + N_BLACK = 1 + BLOCK_Ax = MAX_AAINDEX + + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] + + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] + + ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] + ai = AAIDX[ws[1]][ws[2]] + + if idx_is_empty(ki) or idx_is_empty(ai): + return NOINDEX + + return ki * BLOCK_Ax + ai + +def kakb_pctoindex(c: Request) -> int: + BLOCK_Ax = 64 * 64 + BLOCK_Bx = 64 + + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] + + ws = c.white_piece_squares[:] + bs = c.black_piece_squares[:] + + if (ft & 1) != 0: + ws[0] = flip_we(ws[0]) + ws[1] = flip_we(ws[1]) + bs[0] = flip_we(bs[0]) + bs[1] = flip_we(bs[1]) + + if (ft & 2) != 0: + ws[0] = flip_ns(ws[0]) + ws[1] = flip_ns(ws[1]) + bs[0] = flip_ns(bs[0]) + bs[1] = flip_ns(bs[1]) + + if (ft & 4) != 0: + ws[0] = flip_nw_se(ws[0]) + ws[1] = flip_nw_se(ws[1]) + bs[0] = flip_nw_se(bs[0]) + bs[1] = flip_nw_se(bs[1]) + + ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] + + if idx_is_empty(ki): + return NOINDEX + + return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + bs[1] + +def kpk_pctoindex(c: Request) -> int: + BLOCK_A = 64 * 64 + BLOCK_B = 64 + + pawn = c.white_piece_squares[1] + wk = c.white_piece_squares[0] + bk = c.black_piece_squares[0] + + if not (chess.A2 <= pawn < chess.A8): + return NOINDEX + + if (pawn & 7) > 3: + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + + sq = pawn + sq ^= 56 # flip_ns + 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: + BLOCK_A = 64 * 64 + BLOCK_B = 64 + + wk = c.white_piece_squares[0] + pawn_a = c.white_piece_squares[1] + pawn_b = c.white_piece_squares[2] + pawn_c = c.white_piece_squares[3] + + bk = c.black_piece_squares[0] + + i = pawn_a - 8 + j = pawn_b - 8 + k = pawn_c - 8 + + ppp48_slice = PPP48_IDX[i][j][k] + + if idx_is_empty(ppp48_slice): + wk = flip_we(wk) + pawn_a = flip_we(pawn_a) + pawn_b = flip_we(pawn_b) + pawn_c = flip_we(pawn_c) + bk = flip_we(bk) + + i = pawn_a - 8 + j = pawn_b - 8 + k = pawn_c - 8 + + ppp48_slice = PPP48_IDX[i][j][k] + + if idx_is_empty(ppp48_slice): + return NOINDEX + + return ppp48_slice * BLOCK_A + wk * BLOCK_B + bk + + +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), +} + + +def sortlists(ws: List[int], wp: List[int]) -> Tuple[List[int], List[int]]: + z = sorted(zip(wp, ws), key=lambda x: x[0], reverse=True) + wp2, ws2 = zip(*z) + return list(ws2), list(wp2) + +def egtb_block_unpack(side: int, n: int, bp: bytes) -> List[int]: + return [dtm_unpack(side, i) for i in bp[:n]] + +def split_index(i: int) -> Tuple[int, int]: + return divmod(i, ENTRIES_PER_BLOCK) + + +tb_DRAW = 0 +tb_WMATE = 1 +tb_BMATE = 2 +tb_FORBID = 3 +tb_UNKNOWN = 7 +iDRAW = tb_DRAW +iWMATE = tb_WMATE +iBMATE = tb_BMATE +iFORBID = tb_FORBID + +iDRAWt = tb_DRAW | 4 +iWMATEt = tb_WMATE | 4 +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 + +def dtm_unpack(stm: int, packed: int) -> int: + p = packed + + if p in [iDRAW, iFORBID]: + return p + + info = p & 3 + store = p >> 2 + + if stm == 0: + if info == iWMATE: + moves = store + 1 + plies = moves * 2 - 1 + prefx = info + elif info == iBMATE: + moves = store + plies = moves * 2 + prefx = info + elif info == iDRAW: + moves = store + 1 + 63 + plies = moves * 2 - 1 + prefx = iWMATE + elif info == iFORBID: + moves = store + 63 + plies = moves * 2 + prefx = iBMATE + else: + plies = 0 + prefx = 0 + + ret = prefx | (plies << 3) + else: + if info == iBMATE: + moves = store + 1 + plies = moves * 2 - 1 + prefx = info + elif info == iWMATE: + moves = store + plies = moves * 2 + prefx = info + elif info == iDRAW: + if store == 63: + # Exception: no position in the 5-man TBs needs to store 63 for + # iBMATE. It is then just used to indicate iWMATE. + store += 1 + + moves = store + 63 + plies = moves * 2 + prefx = iWMATE + else: + moves = store + 1 + 63 + plies = moves * 2 - 1 + prefx = iBMATE + elif info == iFORBID: + moves = store + 63 + plies = moves * 2 + prefx = iWMATE + else: + plies = 0 + prefx = 0 + + ret = prefx | (plies << 3) + + return ret + + +class MissingTableError(KeyError): + """Can not probe position because a required table is missing.""" + + +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 + + +class Request: + egkey: str + white_piece_squares: List[int] + white_piece_types: List[int] + black_piece_squares: List[int] + black_piece_types: List[int] + is_reversed: bool + + def __init__(self, white_squares: List[int], white_types: List[chess.PieceType], black_squares: List[int], black_types: List[chess.PieceType], side: int, epsq: 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 + + +@dataclasses.dataclass +class ZipInfo: + extraoffset: int + totalblocks: int + blockindex: List[int] + + +class PythonTablebase: + """Provides access to Gaviota tablebases using pure Python code.""" + + def __init__(self) -> None: + self.available_tables: Dict[str, str] = {} + + self.streams: Dict[str, BinaryIO] = {} + self.zipinfo: Dict[str, ZipInfo] = {} + + self.block_cache: Dict[Tuple[str, int, int], TableBlock] = {} + self.block_age = 0 + + def add_directory(self, directory: str) -> None: + """ + Adds *.gtb.cp4* tables from a directory. The relevant files are lazily + opened when the tablebase is actually probed. + """ + directory = os.path.abspath(directory) + if not os.path.isdir(directory): + raise IOError(f"not a directory: {directory!r}") + + for tbfile in fnmatch.filter(os.listdir(directory), "*.gtb.cp4"): + self.available_tables[os.path.basename(tbfile).replace(".gtb.cp4", "")] = os.path.join(directory, tbfile) + + def probe_dtm(self, board: 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. + + In the example position, white to move will get mated in 10 half-moves: + + >>> import chess + >>> import chess.gaviota + >>> + >>> with chess.gaviota.open_tablebase("data/gaviota") as tablebase: + ... board = chess.Board("8/8/8/8/8/8/8/K2kr3 w - - 0 1") + ... print(tablebase.probe_dtm(board)) + ... + -10 + + :raises: :exc:`KeyError` (or specifically + :exc:`chess.gaviota.MissingTableError`) if the probe fails. Use + :func:`~chess.gaviota.PythonTablebase.get_dtm()` if you prefer + to get ``None`` instead of an exception. + + Note that probing a corrupted table file is undefined behavior. + """ + # Can not probe positions with castling rights. + if board.castling_rights: + raise KeyError(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()}") + + # KvK is a draw. + if board.occupied == board.kings: + return 0 + + # 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) + + # Probe. + dtm = self.egtb_get_dtm(req) + ply, res = unpackdist(dtm) + + if res == iWMATE: + # White mates in the stored position. + if req.realside == 1: + if req.is_reversed: + return ply + else: + return -ply + else: + if req.is_reversed: + return -ply + else: + return ply + elif res == iBMATE: + # Black mates in the stored position. + if req.realside == 0: + if req.is_reversed: + return ply + else: + return -ply + else: + if req.is_reversed: + return -ply + else: + return ply + else: + # Draw. + return 0 + + def get_dtm(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: + try: + return self.probe_dtm(board) + except KeyError: + return default + + def probe_wdl(self, board: chess.Board) -> int: + """ + 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. + + >>> import chess + >>> import chess.gaviota + >>> + >>> with chess.gaviota.open_tablebase("data/gaviota") as tablebase: + ... board = chess.Board("8/4k3/8/B7/8/8/8/4K3 w - - 0 1") + ... print(tablebase.probe_wdl(board)) + ... + 0 + + :raises: :exc:`KeyError` (or specifically + :exc:`chess.gaviota.MissingTableError`) if the probe fails. Use + :func:`~chess.gaviota.PythonTablebase.get_wdl()` if you prefer + to get ``None`` instead of an exception. + + Note that probing a corrupted table file is undefined behavior. + """ + dtm = self.probe_dtm(board) + + if dtm == 0: + if board.is_checkmate(): + return -1 + else: + return 0 + elif dtm > 0: + return 1 + else: + return -1 + + def get_wdl(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: + try: + return self.probe_wdl(board) + except KeyError: + return default + + def _setup_tablebase(self, req: Request) -> BinaryIO: + white_letters = "".join(chess.piece_symbol(i) for i in req.white_types) + black_letters = "".join(chess.piece_symbol(i) for i in req.black_types) + + if (white_letters + black_letters) in self.available_tables: + req.is_reversed = False + req.egkey = white_letters + black_letters + req.white_piece_squares = req.white_squares + req.white_piece_types = req.white_types + req.black_piece_squares = req.black_squares + req.black_piece_types = req.black_types + elif (black_letters + white_letters) in self.available_tables: + req.is_reversed = True + req.egkey = black_letters + white_letters + req.white_piece_squares = [flip_ns(s) for s in req.black_squares] + 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()}") + + return self._open_tablebase(req) + + def _open_tablebase(self, req: Request) -> BinaryIO: + stream = self.streams.get(req.egkey) + + if stream is None: + path = self.available_tables[req.egkey] + stream = open(path, "rb+") + self.egtb_loadindexes(req.egkey, stream) + self.streams[req.egkey] = stream + + return stream + + def close(self) -> None: + """Closes all loaded tables.""" + self.available_tables.clear() + + self.zipinfo.clear() + + self.block_age = 0 + self.block_cache.clear() + + while self.streams: + _, 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) + + # Choose 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 + + blocks_per_side = 1 + (maxindex - 1) // ENTRIES_PER_BLOCK + block_in_side = idx // ENTRIES_PER_BLOCK + + return req.side * blocks_per_side + block_in_side + + def egtb_block_getsize(self, req: Request, idx: int) -> int: + blocksz = ENTRIES_PER_BLOCK + maxindex = EGKEY[req.egkey].maxindex + block = idx // blocksz + offset = block * blocksz + + if (offset + blocksz) > maxindex: + return maxindex - offset # Last block size + else: + return blocksz # Size of a normal block + + def _tb_probe(self, req: Request) -> int: + stream = self._setup_tablebase(req) + idx = EGKEY[req.egkey].pctoi(req) + offset, remainder = split_index(idx) + + t = self.block_cache.get((req.egkey, offset, req.side)) + + if t is None: + t = TableBlock(req.egkey, req.side, offset, self.block_age) + + block = self.egtb_block_getnumber(req, idx) + n = self.egtb_block_getsize(req, idx) + z = self.egtb_block_getsize_zipped(req.egkey, block) + + self.egtb_block_park(req.egkey, block, stream) + buffer_zipped = stream.read(z) + + if buffer_zipped[0] == 0: + # If flag is zero, plain LZMA is following. + buffer_zipped = buffer_zipped[2:] + else: + # Else LZMA86. Build a fake header. + DICTIONARY_SIZE = 4096 + POS_STATE_BITS = 2 + NUM_LITERAL_POS_STATE_BITS = 0 + NUM_LITERAL_CONTEXT_BITS = 3 + properties = bytearray(13) + properties[0] = (POS_STATE_BITS * 5 + NUM_LITERAL_POS_STATE_BITS) * 9 + NUM_LITERAL_CONTEXT_BITS + for i in range(4): + properties[1 + i] = (DICTIONARY_SIZE >> (8 * i)) & 0xFF + for i in range(8): + properties[5 + i] = (n >> (8 * i)) & 0xFF + + # Concatenate the fake header with the true LZMA stream. + buffer_zipped = properties + buffer_zipped[15:] + + buffer_packed = lzma.LZMADecompressor().decompress(buffer_zipped) + + t.pcache = egtb_block_unpack(req.side, n, buffer_packed) + + # Update LRU block cache. + self.block_cache[(t.egkey, t.offset, t.side)] = t + if len(self.block_cache) > 128: + lru_cache_key = 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 + + self.block_age += 1 + dtm = t.pcache[remainder] + + return dtm + + def egtb_loadindexes(self, egkey: str, stream: BinaryIO) -> ZipInfo: + zipinfo = self.zipinfo.get(egkey) + + if zipinfo is None: + # Get reserved bytes, blocksize, offset. + stream.seek(0) + HeaderStruct = struct.Struct("<10I") + header = HeaderStruct.unpack(stream.read(HeaderStruct.size)) + offset = header[8] + + blocks = ((offset - 40) // 4) - 1 + n_idx = blocks + 1 + + IndexStruct = struct.Struct("<" + "I" * n_idx) + p = list(IndexStruct.unpack(stream.read(IndexStruct.size))) + + zipinfo = ZipInfo(extraoffset=0, totalblocks=n_idx, blockindex=p) + self.zipinfo[egkey] = zipinfo + + return zipinfo + + def egtb_block_getsize_zipped(self, egkey: str, block: int) -> int: + i = self.zipinfo[egkey].blockindex[block] + j = self.zipinfo[egkey].blockindex[block + 1] + return j - i + + def egtb_block_park(self, egkey: str, block: int, stream: BinaryIO) -> int: + i = self.zipinfo[egkey].blockindex[block] + i += self.zipinfo[egkey].extraoffset + stream.seek(i) + return i + + def __enter__(self) -> PythonTablebase: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.close() + + +class NativeTablebase: + """ + Provides access to Gaviota tablebases via the shared library libgtb. + Has the same interface as :class:`~chess.gaviota.PythonTablebase`. + """ + + def __init__(self, libgtb: ctypes.CDLL) -> None: + self.paths: List[str] = [] + + self.libgtb = libgtb + self.libgtb.tb_init.restype = ctypes.c_char_p + self.libgtb.tb_restart.restype = ctypes.c_char_p + self.libgtb.tbpaths_getmain.restype = ctypes.c_char_p + self.libgtb.tb_probe_hard.argtypes = [ + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_ubyte), + ctypes.POINTER(ctypes.c_ubyte), + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_uint) + ] + + if self.libgtb.tb_is_initialized(): + raise RuntimeError("only one gaviota instance can be initialized at a time") + + self._tbcache_restart(1024 * 1024, 50) + + def add_directory(self, directory: str) -> None: + if not os.path.isdir(directory): + raise IOError(f"not a directory: {directory!r}") + + self.paths.append(directory) + self._tb_restart() + + def _tb_restart(self) -> None: + self.c_paths = (ctypes.c_char_p * len(self.paths))() + self.c_paths[:] = [path.encode("utf-8") for path in self.paths] + + verbosity = ctypes.c_int(1) + compression_scheme = ctypes.c_int(4) + + ret = self.libgtb.tb_restart(verbosity, compression_scheme, self.c_paths) + if ret: + LOGGER.debug(ret.decode("utf-8")) + + LOGGER.debug("Main path has been set to %r", self.libgtb.tbpaths_getmain().decode("utf-8")) + + av = self.libgtb.tb_availability() + if av & 1: + LOGGER.debug("Some 3-piece tables available") + if av & 2: + LOGGER.debug("All 3-piece tables complete") + if av & 4: + LOGGER.debug("Some 4-piece tables available") + if av & 8: + LOGGER.debug("All 4-piece tables complete") + if av & 16: + LOGGER.debug("Some 5-piece tables available") + if av & 32: + 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)) + + def probe_dtm(self, board: chess.Board) -> int: + return self._probe_hard(board) + + def probe_wdl(self, board: chess.Board) -> int: + return self._probe_hard(board, wdl_only=True) + + def get_dtm(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: + try: + return self.probe_dtm(board) + except KeyError: + return default + + def get_wdl(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: + try: + return self.probe_wdl(board) + except KeyError: + return default + + def _probe_hard(self, board: chess.Board, wdl_only: bool = False) -> int: + if board.is_insufficient_material(): + return 0 + + if board.castling_rights: + raise KeyError(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()}") + + 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) + castling = ctypes.c_uint(0) + + c_ws = (ctypes.c_uint * 17)() + c_wp = (ctypes.c_ubyte * 17)() + + i = -1 + for i, square in enumerate(chess.SquareSet(board.occupied_co[chess.WHITE])): + c_ws[i] = square + c_wp[i] = typing.cast(chess.PieceType, board.piece_type_at(square)) + + c_ws[i + 1] = 64 + c_wp[i + 1] = 0 + + c_bs = (ctypes.c_uint * 17)() + c_bp = (ctypes.c_ubyte * 17)() + + i = -1 + for i, square in enumerate(chess.SquareSet(board.occupied_co[chess.BLACK])): + c_bs[i] = square + c_bp[i] = typing.cast(chess.PieceType, board.piece_type_at(square)) + + c_bs[i + 1] = 64 + c_bp[i + 1] = 0 + + # Do a hard probe. + info = ctypes.c_uint() + pliestomate = ctypes.c_uint() + if not wdl_only: + ret = self.libgtb.tb_probe_hard(stm, ep_square, castling, c_ws, c_bs, c_wp, c_bp, ctypes.byref(info), ctypes.byref(pliestomate)) + dtm = int(pliestomate.value) + else: + ret = self.libgtb.tb_probe_WDL_hard(stm, ep_square, castling, c_ws, c_bs, c_wp, c_bp, ctypes.byref(info)) + dtm = 1 + + # Probe forbidden. + if info.value == 3: + raise MissingTableError(f"gaviota table for {board.fen()} not available") + + # Draw. + if ret and info.value == 0: + return 0 + + # White mates. + if ret and info.value == 1: + return dtm if board.turn == chess.WHITE else -dtm + + # Black mates. + if ret and info.value == 2: + return dtm if board.turn == chess.BLACK else -dtm + + raise KeyError(f"gaviota probe failed for {board.fen()}") + + def close(self) -> None: + self.paths = [] + + if self.libgtb.tb_is_initialized(): + self.libgtb.tbcache_done() + self.libgtb.tb_done() + + def __enter__(self) -> 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: Optional[str] = None, LibraryLoader: ctypes.LibraryLoader[ctypes.CDLL] = ctypes.cdll) -> NativeTablebase: + """ + Opens a collection of tables for probing using libgtb. + + In most cases :func:`~chess.gaviota.open_tablebase()` should be used. + Use this function only if you do not want to downgrade to pure Python + tablebase probing. + + :raises: :exc:`RuntimeError` or :exc:`OSError` when libgtb can not be used. + """ + libgtb = libgtb or ctypes.util.find_library("gtb") or "libgtb.so.1.0.1" + tables = NativeTablebase(LibraryLoader.LoadLibrary(libgtb)) + tables.add_directory(directory) + return tables + + +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. + + First native access via the shared library libgtb is tried. You can + optionally provide a specific library name or a library loader. + The shared library has global state and caches, so only one instance can + be open at a time. + + Second, pure Python probing code is tried. + """ + try: + if LibraryLoader: + return open_tablebase_native(directory, libgtb=libgtb, LibraryLoader=LibraryLoader) + except (OSError, RuntimeError) as err: + LOGGER.info("Falling back to pure Python tablebase: %r", err) + + tables = PythonTablebase() + tables.add_directory(directory) + return tables diff --git a/cartesi-python-chess-cartesi-img/chess/pgn.py b/cartesi-python-chess-cartesi-img/chess/pgn.py new file mode 100644 index 0000000..fccf9f2 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/pgn.py @@ -0,0 +1,1694 @@ +# This file is part of the python-chess library. +# Copyright (C) 2012-2021 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 enum +import itertools +import logging +import re +import typing + +import chess +import chess.engine +import chess.svg + +from typing import Any, Callable, Dict, Generic, Iterable, Iterator, List, Mapping, MutableMapping, Set, TextIO, Tuple, Type, TypeVar, Optional, Union +from chess import Color, Square + +try: + from typing import Literal + _TrueLiteral = Literal[True] +except ImportError: + # Before Python 3.8. + _TrueLiteral = bool # type: ignore + + +LOGGER = logging.getLogger(__name__) + + +# Reference of Numeric Annotation Glyphs (NAGs): +# https://en.wikipedia.org/wiki/Numeric_Annotation_Glyphs + +NAG_NULL = 0 + +NAG_GOOD_MOVE = 1 +"""A good move. Can also be indicated by ``!`` in PGN notation.""" + +NAG_MISTAKE = 2 +"""A mistake. Can also be indicated by ``?`` in PGN notation.""" + +NAG_BRILLIANT_MOVE = 3 +"""A brilliant move. Can also be indicated by ``!!`` in PGN notation.""" + +NAG_BLUNDER = 4 +"""A blunder. Can also be indicated by ``??`` in PGN notation.""" + +NAG_SPECULATIVE_MOVE = 5 +"""A speculative move. Can also be indicated by ``!?`` in PGN notation.""" + +NAG_DUBIOUS_MOVE = 6 +"""A dubious move. Can also be indicated by ``?!`` in PGN notation.""" + +NAG_FORCED_MOVE = 7 +NAG_SINGULAR_MOVE = 8 +NAG_WORST_MOVE = 9 +NAG_DRAWISH_POSITION = 10 +NAG_QUIET_POSITION = 11 +NAG_ACTIVE_POSITION = 12 +NAG_UNCLEAR_POSITION = 13 +NAG_WHITE_SLIGHT_ADVANTAGE = 14 +NAG_BLACK_SLIGHT_ADVANTAGE = 15 +NAG_WHITE_MODERATE_ADVANTAGE = 16 +NAG_BLACK_MODERATE_ADVANTAGE = 17 +NAG_WHITE_DECISIVE_ADVANTAGE = 18 +NAG_BLACK_DECISIVE_ADVANTAGE = 19 + +NAG_WHITE_ZUGZWANG = 22 +NAG_BLACK_ZUGZWANG = 23 + +NAG_WHITE_MODERATE_COUNTERPLAY = 132 +NAG_BLACK_MODERATE_COUNTERPLAY = 133 +NAG_WHITE_DECISIVE_COUNTERPLAY = 134 +NAG_BLACK_DECISIVE_COUNTERPLAY = 135 +NAG_WHITE_MODERATE_TIME_PRESSURE = 136 +NAG_BLACK_MODERATE_TIME_PRESSURE = 137 +NAG_WHITE_SEVERE_TIME_PRESSURE = 138 +NAG_BLACK_SEVERE_TIME_PRESSURE = 139 + +NAG_NOVELTY = 146 + + +TAG_REGEX = re.compile(r"^\[([A-Za-z0-9_]+)\s+\"([^\r]*)\"\]\s*$") + +TAG_NAME_REGEX = re.compile(r"^[A-Za-z0-9_]+\Z") + +MOVETEXT_REGEX = re.compile(r""" + ( + [NBKRQ]?[a-h]?[1-8]?[\-x]?[a-h][1-8](?:=?[nbrqkNBRQK])? + |[PNBRQK]?@[a-h][1-8] + |-- + |Z0 + |0000 + |@@@@ + |O-O(?:-O)? + |0-0(?:-0)? + ) + |(\{.*) + |(;.*) + |(\$[0-9]+) + |(\() + |(\)) + |(\*|1-0|0-1|1/2-1/2) + |([\?!]{1,2}) + """, re.DOTALL | re.VERBOSE) + +SKIP_MOVETEXT_REGEX = re.compile(r""";|\{|\}""") + + +CLOCK_REGEX = re.compile(r"""\[%clk\s(\d+):(\d+):(\d+(?:\.\d*)?)\]""") + +EVAL_REGEX = re.compile(r""" + \[%eval\s(?: + \#([+-]?\d+) + |([+-]?(?:\d{0,10}\.\d{1,2}|\d{1,10}\.?)) + )(?: + ,(\d+) + )?\] + """, re.VERBOSE) + +ARROWS_REGEX = re.compile(r""" + \[%(?:csl|cal)\s( + [RGYB][a-h][1-8](?:[a-h][1-8])? + (?:,[RGYB][a-h][1-8](?:[a-h][1-8])?)* + )\] + """, re.VERBOSE) + + +TAG_ROSTER = ["Event", "Site", "Date", "Round", "White", "Black", "Result"] + + +class SkipType(enum.Enum): + SKIP = None + +SKIP = SkipType.SKIP + + +ResultT = TypeVar("ResultT", covariant=True) + + +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): + parent: Optional[GameNode] + """The parent node or ``None`` if this is the root node of the game.""" + + move: Optional[chess.Move] + """ + The move leading to this node or ``None`` if this is the root node of the + game. + """ + + variations: List[ChildNode] + """A list of child nodes.""" + + comment: 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_comment: str + nags: Set[int] + + def __init__(self, *, comment: str = "") -> None: + self.parent = None + self.move = None + self.variations = [] + self.comment = comment + + # Deprecated: These should be properties of ChildNode, but need to + # remain here for backwards compatibility. + self.starting_comment = "" + self.nags = set() + + @abc.abstractmethod + def board(self) -> chess.Board: + """ + Gets a board with the position of the node. + + For the root node, this is the default starting position (for the + ``Variant``) unless the ``FEN`` header tag is set. + + It's a copy, so modifying the board will not alter the game. + """ + + @abc.abstractmethod + def ply(self) -> int: + """ + 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()`. + + Usually this is equal to the number of parent nodes, but it may be + more if the game was started from a custom position. + """ + + def turn(self) -> Color: + """ + Gets the color to move at this node. See :data:`chess.Board.turn`. + """ + return self.ply() % 2 == 0 + + 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.""" + 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.""" + node = self + + while node.variations: + node = node.variations[0] + + return node + + def is_end(self) -> bool: + """Checks if this node is the last node in the current variation.""" + return not self.variations + + def starts_variation(self) -> bool: + """ + Checks if this node starts a variation (and can thus have a starting + comment). The root node does not start a variation and can have no + starting comment. + + For example, in ``1. e4 e5 (1... c5 2. Nf3) 2. Nf3``, the node holding + 1... c5 starts a variation. + """ + if not self.parent or not self.parent.variations: + return False + + return self.parent.variations[0] != self + + def is_mainline(self) -> bool: + """Checks if the node is in the mainline of the game.""" + node = self + + while node.parent: + parent = node.parent + + if not parent.variations or parent.variations[0] != node: + return False + + node = parent + + return True + + 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. + """ + 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]) -> ChildNode: + try: + return self.variations[move] # type: ignore + except TypeError: + for variation in self.variations: + if variation.move == move or variation == move: + return variation + + raise KeyError(move) + + def __contains__(self, move: Union[int, chess.Move, GameNode]) -> bool: + try: + self[move] + except KeyError: + return False + else: + return True + + def variation(self, move: Union[int, chess.Move, GameNode]) -> ChildNode: + """ + Gets a child node by either the move or the variation index. + """ + return self[move] + + 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: 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: 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: 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: 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] = []) -> ChildNode: + """Creates a child node with the given attributes.""" + # Instanciate 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 = "", 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, nags=nags) + self.variations.insert(0, self.variations.pop()) + return 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. + """ + return self.variations[0] if self.variations else None + + def mainline(self) -> Mainline[ChildNode]: + """Returns an iterable over the mainline starting after this node.""" + return Mainline(self, lambda node: node) + + def mainline_moves(self) -> Mainline[chess.Move]: + """Returns an iterable over the main moves after this node.""" + return Mainline(self, lambda node: node.move) + + def add_line(self, moves: Iterable[chess.Move], *, comment: str = "", starting_comment: 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. + """ + node = self + + # Add line. + for move in moves: + node = node.add_variation(move, starting_comment=starting_comment) + starting_comment = "" + + # Merge comment and NAGs. + if node.comment: + node.comment += " " + comment + else: + node.comment = comment + + node.nags.update(nags) + + return node + + def eval(self) -> Optional[chess.engine.PovScore]: + """ + Parses the first valid ``[%eval ...]`` annotation in the comment of + this node, if any. + """ + match = EVAL_REGEX.search(self.comment) + if not match: + return None + + turn = self.turn() + + if match.group(1): + mate = int(match.group(1)) + 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(int(float(match.group(2)) * 100)) + + 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. + """ + match = EVAL_REGEX.search(self.comment) + return int(match.group(3)) if match and match.group(3) else None + + def set_eval(self, score: Optional[chess.engine.PovScore], depth: Optional[int] = None) -> None: + """ + Replaces the first valid ``[%eval ...]`` annotation in the comment of + this node or adds a new one. + """ + eval = "" + if score is not None: + depth_suffix = "" if depth is None else f",{max(depth, 0):d}" + cp = score.white().score() + if cp is not None: + eval = f"[%eval {float(cp) / 100:.2f}{depth_suffix}]" + elif score.white().mate(): + eval = f"[%eval #{score.white().mate()}{depth_suffix}]" + + self.comment, found = EVAL_REGEX.subn(eval, self.comment, count=1) + + if not found and eval: + if self.comment and not self.comment.endswith(" "): + self.comment += " " + self.comment += eval + + def arrows(self) -> List[chess.svg.Arrow]: + """ + Parses all ``[%csl ...]`` and ``[%cal ...]`` annotations in the comment + of this node. + + Returns a list of :class:`arrows `. + """ + arrows = [] + for match in ARROWS_REGEX.finditer(self.comment): + for group in match.group(1).split(","): + arrows.append(chess.svg.Arrow.from_pgn(group)) + + return arrows + + 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] = [] + + 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 + + self.comment = ARROWS_REGEX.sub("", self.comment).strip() + + prefix = "" + if csl: + prefix += f"[%csl {','.join(csl)}]" + if cal: + prefix += f"[%cal {','.join(cal)}]" + + if prefix: + self.comment = prefix + " " + self.comment if self.comment else 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(self.comment) + if match is None: + return None + return int(match.group(1)) * 3600 + int(match.group(2)) * 60 + float(match.group(3)) + + 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.comment, found = CLOCK_REGEX.subn(clk, self.comment, count=1) + + if not found and clk: + if self.comment and not self.comment.endswith(" "): + self.comment += " " + self.comment += clk + + @abc.abstractmethod + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: + """ + Traverses game nodes in PGN order using the given *visitor*. Starts with + the move leading to this node. Returns the *visitor* result. + """ + + def accept_subgame(self, visitor: BaseVisitor[ResultT]) -> ResultT: + """ + Traverses headers and game nodes in PGN order, as if the game was + starting after this node. Returns the *visitor* result. + """ + if visitor.begin_game() is not SKIP: + game = self.game() + board = self.board() + + dummy_game = Game.without_tag_roster() + dummy_game.setup(board) + + visitor.begin_headers() + + for tagname, tagvalue in game.headers.items(): + if tagname not in dummy_game.headers: + visitor.visit_header(tagname, tagvalue) + for tagname, tagvalue in dummy_game.headers.items(): + visitor.visit_header(tagname, tagvalue) + + if visitor.end_headers() is not SKIP: + visitor.visit_board(board) + + if self.variations: + self.variations[0]._accept(board, visitor) + + visitor.visit_result(game.headers.get("Result", "*")) + + visitor.end_game() + return visitor.result() + + def __str__(self) -> str: + return self.accept(StringExporter(columns=None)) + + +class ChildNode(GameNode): + """ + A child node of a game, with the move leading to it. + Extends :class:`~chess.pgn.GameNode`. + """ + + parent: GameNode + """The parent node.""" + + move: chess.Move + """The move leading to this node.""" + + starting_comment: 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: str = "", starting_comment: 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_comment = starting_comment + + 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 + + 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. + """ + 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. + """ + return self.parent.board().uci(self.move, chess960=chess960) + + def end(self) -> ChildNode: + """Follows the main variation to the end and returns the last node.""" + return typing.cast(ChildNode, super().end()) + + def _accept_node(self, parent_board: chess.Board, visitor: BaseVisitor[ResultT]) -> None: + if self.starting_comment: + visitor.visit_comment(self.starting_comment) + + 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.comment: + visitor.visit_comment(self.comment) + + 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() + + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: + self._accept(self.parent.board(), visitor, sidelines=False) + return visitor.result() + + def __repr__(self) -> str: + try: + parent_board = self.parent.board() + except ValueError: + return f"<{type(self).__name__} at {id(self):#x} (dangling: {self.move})>" + else: + return "<{} at {:#x} ({}{} {} ...)>".format( + type(self).__name__, + id(self), + parent_board.fullmove_number, + "." if parent_board.turn == chess.WHITE else "...", + parent_board.san(self.move)) + + +GameT = TypeVar("GameT", bound="Game") + +class Game(GameNode): + """ + The root node of a game with extra information such as headers and the + starting position. Extends :class:`~chess.pgn.GameNode`. + """ + + headers: Headers + """ + A mapping of headers. By default, the following 7 headers are provided + (Seven Tag Roster): + + >>> import chess.pgn + >>> + >>> game = chess.pgn.Game() + >>> game.headers + Headers(Event='?', Site='?', Date='????.??.??', Round='?', White='?', Black='?', Result='*') + """ + + errors: List[Exception] + """ + A list of errors (such as illegal or ambiguous moves) encountered while + parsing the game. + """ + + def __init__(self, headers: Optional[Union[Mapping[str, str], Iterable[Tuple[str, str]]]] = None) -> None: + super().__init__() + self.headers = Headers(headers) + self.errors = [] + + def board(self) -> chess.Board: + return self.headers.board() + + # TODO: Consider naming. + def _interactive_viewer(self) -> Any: + from chess._interactive import InteractiveViewer + return InteractiveViewer(self) # type: ignore + + 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 + ``FEN``, ``SetUp``, and ``Variant`` header tags. + """ + try: + fen = board.fen() # type: ignore + setup = typing.cast(chess.Board, board) + except AttributeError: + setup = chess.Board(board) # type: ignore + setup.chess960 = setup.has_chess960_castling_rights() + fen = setup.fen() + + if fen == type(setup).starting_fen: + self.headers.pop("SetUp", None) + self.headers.pop("FEN", None) + else: + self.headers["SetUp"] = "1" + self.headers["FEN"] = fen + + if type(setup).aliases[0] == "Standard" and setup.chess960: + self.headers["Variant"] = "Chess960" + elif type(setup).aliases[0] != "Standard": + self.headers["Variant"] = type(setup).aliases[0] + self.headers["FEN"] = fen + else: + self.headers.pop("Variant", None) + + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: + """ + Traverses the game in PGN order using the given *visitor*. Returns + the *visitor* result. + """ + if visitor.begin_game() is not SKIP: + for tagname, tagvalue in self.headers.items(): + visitor.visit_header(tagname, tagvalue) + if visitor.end_headers() is not SKIP: + board = self.board() + visitor.visit_board(board) + + if self.comment: + visitor.visit_comment(self.comment) + + if self.variations: + self.variations[0]._accept(board, visitor) + + visitor.visit_result(self.headers.get("Result", "*")) + + visitor.end_game() + return visitor.result() + + @classmethod + def from_board(cls: Type[GameT], board: chess.Board) -> GameT: + """Creates a game from the move stack of a :class:`~chess.Board()`.""" + # Setup the initial position. + game = cls() + game.setup(board.root()) + node: GameNode = game + + # Replay all moves. + for move in board.move_stack: + node = node.add_variation(move) + + game.headers["Result"] = board.result() + return game + + @classmethod + def without_tag_roster(cls: Type[GameT]) -> GameT: + """Creates an empty game without the default Seven Tag Roster.""" + return cls(headers={}) + + @classmethod + def builder(cls: Type[GameT]) -> GameBuilder[GameT]: + return GameBuilder(Game=cls) + + def __repr__(self) -> str: + return "<{} at {:#x} ({!r} vs. {!r}, {!r}{})>".format( + type(self).__name__, + id(self), + self.headers.get("White", "?"), + self.headers.get("Black", "?"), + self.headers.get("Date", "????.??.??"), + f", {len(self.errors)} errors" if self.errors else "") + + +HeadersT = TypeVar("HeadersT", bound="Headers") + +class Headers(MutableMapping[str, str]): + def __init__(self, data: Optional[Union[Mapping[str, str], Iterable[Tuple[str, str]]]] = None, **kwargs: str) -> None: + self._tag_roster: Dict[str, str] = {} + self._others: Dict[str, str] = {} + + if data is None: + data = { + "Event": "?", + "Site": "?", + "Date": "????.??.??", + "Round": "?", + "White": "?", + "Black": "?", + "Result": "*" + } + + self.update(data, **kwargs) + + def is_chess960(self) -> bool: + return self.get("Variant", "").lower() in [ + "chess960", + "chess 960", + "fischerandom", # Cute Chess + "fischerrandom", + "fischer random", + ] + + def is_wild(self) -> bool: + # http://www.freechess.org/Help/HelpFiles/wild.html + return self.get("Variant", "").lower() in [ + "wild/0", "wild/1", "wild/2", "wild/3", "wild/4", "wild/5", + "wild/6", "wild/7", "wild/8", "wild/8a"] + + def variant(self) -> Type[chess.Board]: + if "Variant" not in self or self.is_chess960() or self.is_wild(): + return chess.Board + else: + from chess.variant import find_variant + return find_variant(self["Variant"]) + + def board(self) -> chess.Board: + VariantBoard = self.variant() + fen = self.get("FEN", VariantBoard.starting_fen) + board = VariantBoard(fen, chess960=self.is_chess960()) + board.chess960 = board.chess960 or board.has_chess960_castling_rights() + return board + + def __setitem__(self, key: str, value: str) -> None: + if key in TAG_ROSTER: + self._tag_roster[key] = value + elif not TAG_NAME_REGEX.match(key): + raise ValueError(f"non-alphanumeric 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] + + def __delitem__(self, key: str) -> None: + if key in TAG_ROSTER: + del self._tag_roster[key] + else: + del self._others[key] + + def __iter__(self) -> Iterator[str]: + for key in TAG_ROSTER: + if key in self._tag_roster: + yield key + + yield from sorted(self._others) + + def __len__(self) -> int: + return len(self._tag_roster) + len(self._others) + + def copy(self: HeadersT) -> HeadersT: + return type(self)(self) + + def __copy__(self: HeadersT) -> HeadersT: + return self.copy() + + def __repr__(self) -> str: + return "{}({})".format( + type(self).__name__, + ", ".join("{}={!r}".format(key, value) for key, value in self.items())) + + @classmethod + def builder(cls: Type[HeadersT]) -> HeadersBuilder[HeadersT]: + return HeadersBuilder(Headers=cls) + + +MainlineMapT = TypeVar("MainlineMapT") + +class Mainline(Generic[MainlineMapT]): + def __init__(self, start: GameNode, f: Callable[[ChildNode], MainlineMapT]) -> None: + self.start = start + self.f = f + + def __bool__(self) -> bool: + return bool(self.start.variations) + + def __iter__(self) -> Iterator[MainlineMapT]: + node = self.start + while node.variations: + node = node.variations[0] + yield self.f(node) + + def __reversed__(self) -> Iterator[MainlineMapT]: + node = self.start.end() + while node.parent and node != self.start: + yield self.f(typing.cast(ChildNode, node)) + node = node.parent + + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: + node = self.start + board = self.start.board() + while node.variations: + node = node.variations[0] + node._accept_node(board, visitor) + board.push(node.move) + return visitor.result() + + def __str__(self) -> str: + return self.accept(StringExporter(columns=None)) + + def __repr__(self) -> str: + return f"" + + +class BaseVisitor(abc.ABC, Generic[ResultT]): + """ + Base class for visitors. + + Use with :func:`chess.pgn.Game.accept()` or + :func:`chess.pgn.GameNode.accept()` or :func:`chess.pgn.read_game()`. + + The methods are called in PGN order. + """ + + def begin_game(self) -> Optional[SkipType]: + """Called at the start of a game.""" + pass + + def begin_headers(self) -> Optional[Headers]: + """Called before visiting game headers.""" + pass + + def visit_header(self, tagname: str, tagvalue: str) -> None: + """Called for each game header.""" + pass + + def end_headers(self) -> Optional[SkipType]: + """Called after visiting game headers.""" + pass + + def parse_san(self, board: chess.Board, san: str) -> chess.Move: + """ + 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. + + .. deprecated:: 1.1 + This method is very limited, because it is only called on moves + that the parser recognizes in the first place. Instead of adding + workarounds here, please report common quirks so that + they can be handled for everyone. + """ + return board.parse_san(san) + + def visit_move(self, board: chess.Board, move: chess.Move) -> None: + """ + Called for each move. + + *board* is the board state before the move. The board state must be + restored before the traversal continues. + """ + pass + + def visit_board(self, board: chess.Board) -> None: + """ + Called for the starting position of the game and after each move. + + The board state must be restored before the traversal continues. + """ + pass + + def visit_comment(self, comment: str) -> None: + """Called for each comment.""" + pass + + def visit_nag(self, nag: int) -> None: + """Called for each NAG.""" + pass + + def begin_variation(self) -> Optional[SkipType]: + """ + Called at the start of a new variation. It is not called for the + mainline of the game. + """ + pass + + def end_variation(self) -> None: + """Concludes a variation.""" + pass + + def visit_result(self, result: str) -> None: + """ + Called at the end of a game with the value from the ``Result`` header. + """ + pass + + def end_game(self) -> None: + """Called at the end of a game.""" + pass + + @abc.abstractmethod + def result(self) -> ResultT: + """Called to get the result of the visitor.""" + + def handle_error(self, error: Exception) -> None: + """Called for encountered errors. Defaults to raising an exception.""" + raise error + + +class GameBuilder(BaseVisitor[GameT]): + """ + Creates a game model. Default visitor for :func:`~chess.pgn.read_game()`. + """ + + @typing.overload + def __init__(self: GameBuilder[Game]) -> None: ... + @typing.overload + def __init__(self: GameBuilder[GameT], *, Game: Type[GameT]) -> None: ... + def __init__(self, *, Game: Any = Game) -> None: + self.Game = Game + + def begin_game(self) -> None: + self.game: GameT = self.Game() + + self.variation_stack: List[GameNode] = [self.game] + self.starting_comment = "" + self.in_variation = False + + def begin_headers(self) -> Headers: + return self.game.headers + + def visit_header(self, tagname: str, tagvalue: str) -> None: + self.game.headers[tagname] = tagvalue + + def visit_nag(self, nag: int) -> None: + self.variation_stack[-1].nags.add(nag) + + def begin_variation(self) -> None: + parent = self.variation_stack[-1].parent + assert parent is not None, "begin_variation called, but root node on top of stack" + self.variation_stack.append(parent) + self.in_variation = False + + def end_variation(self) -> None: + self.variation_stack.pop() + + 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: + 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() + else: + # Otherwise, it is a starting comment. + new_comment = [self.starting_comment, comment] + self.starting_comment = "\n".join(new_comment).strip() + + 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.in_variation = True + + def handle_error(self, error: Exception) -> None: + """ + Populates :data:`chess.pgn.Game.errors` with encountered errors and + logs them. + + You can silence the log and handle errors yourself after parsing: + + >>> import chess.pgn + >>> import logging + >>> + >>> logging.getLogger("chess.pgn").setLevel(logging.CRITICAL) + >>> + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn") + >>> + >>> game = chess.pgn.read_game(pgn) + >>> game.errors # List of exceptions + [] + + You can also override this method to hook into error handling: + + >>> import chess.pgn + >>> + >>> class MyGameBuilder(chess.pgn.GameBuilder): + >>> def handle_error(self, error: Exception) -> None: + >>> pass # Ignore error + >>> + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn") + >>> + >>> game = chess.pgn.read_game(pgn, Visitor=MyGameBuilder) + """ + LOGGER.exception("error during pgn parsing") + self.game.errors.append(error) + + def result(self) -> GameT: + """ + Returns the visited :class:`~chess.pgn.Game()`. + """ + return self.game + + +class HeadersBuilder(BaseVisitor[HeadersT]): + """Collects headers into a dictionary.""" + + @typing.overload + def __init__(self: HeadersBuilder[Headers]) -> None: ... + @typing.overload + def __init__(self: HeadersBuilder[HeadersT], *, Headers: Type[Headers]) -> None: ... + def __init__(self, *, Headers: Any = Headers) -> None: + self.Headers = Headers + + def begin_headers(self) -> HeadersT: + self.headers: HeadersT = self.Headers({}) + return self.headers + + def visit_header(self, tagname: str, tagvalue: str) -> None: + self.headers[tagname] = tagvalue + + def end_headers(self) -> SkipType: + return SKIP + + def result(self) -> HeadersT: + return self.headers + + +class BoardBuilder(BaseVisitor[chess.Board]): + """ + Returns the final position of the game. The mainline of the game is + on the move stack. + """ + + def begin_game(self) -> None: + self.skip_variation_depth = 0 + + def begin_variation(self) -> SkipType: + self.skip_variation_depth += 1 + return SKIP + + def end_variation(self) -> None: + self.skip_variation_depth = max(self.skip_variation_depth - 1, 0) + + def visit_board(self, board: chess.Board) -> None: + if not self.skip_variation_depth: + self.board = board + + def result(self) -> chess.Board: + return self.board + + +class SkipVisitor(BaseVisitor[_TrueLiteral]): + """Skips a game.""" + + def begin_game(self) -> SkipType: + return SKIP + + def end_headers(self) -> SkipType: + return SKIP + + def begin_variation(self) -> SkipType: + return SKIP + + def result(self) -> _TrueLiteral: + return True + + +class StringExporterMixin: + def __init__(self, *, columns: Optional[int] = 80, headers: bool = True, comments: bool = True, variations: bool = True): + self.columns = columns + self.headers = headers + self.comments = comments + self.variations = variations + + self.found_headers = False + + self.force_movenumber = True + + self.lines: List[str] = [] + self.current_line = "" + self.variation_depth = 0 + + def flush_current_line(self) -> None: + if self.current_line: + self.lines.append(self.current_line.rstrip()) + self.current_line = "" + + def write_token(self, token: str) -> None: + if self.columns is not None and self.columns - len(self.current_line) < len(token): + self.flush_current_line() + self.current_line += token + + def write_line(self, line: str = "") -> None: + self.flush_current_line() + self.lines.append(line.rstrip()) + + def end_game(self) -> None: + self.write_line() + + def begin_headers(self) -> None: + self.found_headers = False + + def visit_header(self, tagname: str, tagvalue: str) -> None: + if self.headers: + self.found_headers = True + self.write_line(f"[{tagname} \"{tagvalue}\"]") + + def end_headers(self) -> None: + if self.found_headers: + self.write_line() + + def begin_variation(self) -> Optional[SkipType]: + self.variation_depth += 1 + + if self.variations: + self.write_token("( ") + self.force_movenumber = True + return None + else: + return SKIP + + def end_variation(self) -> None: + self.variation_depth -= 1 + + if self.variations: + self.write_token(") ") + self.force_movenumber = True + + def visit_comment(self, comment: str) -> None: + if self.comments and (self.variations or not self.variation_depth): + self.write_token("{ " + comment.replace("}", "").strip() + " } ") + self.force_movenumber = True + + def visit_nag(self, nag: int) -> None: + if self.comments and (self.variations or not self.variation_depth): + self.write_token("$" + str(nag) + " ") + + def visit_move(self, board: chess.Board, move: chess.Move) -> None: + if self.variations or not self.variation_depth: + # Write the move number. + if board.turn == chess.WHITE: + self.write_token(str(board.fullmove_number) + ". ") + elif self.force_movenumber: + self.write_token(str(board.fullmove_number) + "... ") + + # Write the SAN. + self.write_token(board.san(move) + " ") + + self.force_movenumber = False + + def visit_result(self, result: str) -> None: + self.write_token(result + " ") + + +class StringExporter(StringExporterMixin, BaseVisitor[str]): + """ + Allows exporting a game as a string. + + >>> import chess.pgn + >>> + >>> game = chess.pgn.Game() + >>> + >>> exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True) + >>> pgn_string = game.accept(exporter) + + Only *columns* characters are written per line. If *columns* is ``None``, + then the entire movetext will be on a single line. This does not affect + header tags and comments. + + There will be no newline characters at the end of the string. + """ + + def result(self) -> str: + if self.current_line: + return "\n".join(itertools.chain(self.lines, [self.current_line.rstrip()])).rstrip() + else: + return "\n".join(self.lines).rstrip() + + def __str__(self) -> str: + return self.result() + + +class FileExporter(StringExporterMixin, BaseVisitor[int]): + """ + Acts like a :class:`~chess.pgn.StringExporter`, but games are written + directly into a text file. + + There will always be a blank line after each game. Handling encodings is up + to the caller. + + >>> import chess.pgn + >>> + >>> game = chess.pgn.Game() + >>> + >>> new_pgn = open("/dev/null", "w", encoding="utf-8") + >>> exporter = chess.pgn.FileExporter(new_pgn) + >>> game.accept(exporter) + """ + + def __init__(self, handle: TextIO, *, columns: Optional[int] = 80, headers: bool = True, comments: bool = True, variations: bool = True): + super().__init__(columns=columns, headers=headers, comments=comments, variations=variations) + self.handle = handle + + def begin_game(self) -> None: + self.written: int = 0 + super().begin_game() + + def flush_current_line(self) -> None: + if self.current_line: + 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.written += self.handle.write(line.rstrip()) + self.written += self.handle.write("\n") + + def result(self) -> int: + return self.written + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.__repr__() + + +@typing.overload +def read_game(handle: TextIO) -> Optional[Game]: ... +@typing.overload +def read_game(handle: TextIO, *, Visitor: Callable[[], BaseVisitor[ResultT]]) -> Optional[ResultT]: ... +def read_game(handle: TextIO, *, Visitor: Any = GameBuilder) -> Any: + """ + Reads a game from a file opened in text mode. + + >>> import chess.pgn + >>> + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn") + >>> + >>> first_game = chess.pgn.read_game(pgn) + >>> second_game = chess.pgn.read_game(pgn) + >>> + >>> first_game.headers["Event"] + 'IBM Man-Machine, New York USA' + >>> + >>> # Iterate through all moves and play them on a board. + >>> board = first_game.board() + >>> for move in first_game.mainline_moves(): + ... board.push(move) + ... + >>> board + Board('4r3/6P1/2p2P1k/1p6/pP2p1R1/P1B5/2P2K2/3r4 b - - 0 45') + + By using text mode, the parser does not need to handle encodings. It is the + caller's responsibility to open the file with the correct encoding. + PGN files are usually ASCII or UTF-8 encoded, sometimes with BOM (which + this parser automatically ignores). + + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn", encoding="utf-8") + + Use :class:`~io.StringIO` to parse games from a string. + + >>> import io + >>> + >>> pgn = io.StringIO("1. e4 e5 2. Nf3 *") + >>> game = chess.pgn.read_game(pgn) + + The end of a game is determined by a completely blank line or the end of + the file. (Of course, blank lines in comments are possible). + + 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:`overridden `. + + Returns the parsed game or ``None`` if the end of file is reached. + """ + visitor = Visitor() + + found_game = False + skipping_game = False + managed_headers: Optional[Headers] = None + unmanaged_headers: Optional[Headers] = None + + # Ignore leading empty lines and comments. + line = handle.readline().lstrip("\ufeff") + while line.isspace() or line.startswith("%") or line.startswith(";"): + line = handle.readline() + + # Parse game headers. + consecutive_empty_lines = 0 + while line: + # 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 + skipping_game = visitor.begin_game() is SKIP + if not skipping_game: + managed_headers = visitor.begin_headers() + if not isinstance(managed_headers, Headers): + unmanaged_headers = Headers({}) + + if not line.startswith("["): + break + + consecutive_empty_lines = 0 + + if not skipping_game: + tag_match = TAG_REGEX.match(line) + if tag_match: + visitor.visit_header(tag_match.group(1), tag_match.group(2)) + if unmanaged_headers is not None: + unmanaged_headers[tag_match.group(1)] = tag_match.group(2) + else: + break + + line = handle.readline() + + if not found_game: + return None + + if not skipping_game: + skipping_game = visitor.end_headers() is SKIP + + if not skipping_game: + # Chess variant. + headers = managed_headers if unmanaged_headers is None else unmanaged_headers + assert headers is not None, "got neither managed nor unmanaged headers" + try: + VariantBoard = headers.variant() + except ValueError as error: + visitor.handle_error(error) + VariantBoard = chess.Board + + # Initial position. + fen = headers.get("FEN", VariantBoard.starting_fen) + try: + board = VariantBoard(fen, chess960=headers.is_chess960()) + except ValueError as error: + visitor.handle_error(error) + skipping_game = True + else: + board.chess960 = board.chess960 or board.has_chess960_castling_rights() + board_stack = [board] + visitor.visit_board(board) + + # Fast path: Skip entire game. + if skipping_game: + in_comment = False + + while line: + if not in_comment: + if line.isspace(): + break + elif line.startswith("%"): + line = handle.readline() + continue + + for match in SKIP_MOVETEXT_REGEX.finditer(line): + token = match.group(0) + if token == "{": + in_comment = True + elif not in_comment and token == ";": + break + elif token == "}": + in_comment = False + + line = handle.readline() + + visitor.end_game() + return visitor.result() + + # Parse movetext. + skip_variation_depth = 0 + 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() + + for match in MOVETEXT_REGEX.finditer(line): + token = match.group(0) + + if token.startswith("{"): + # Consume until the end of the comment. + line = token[1:] + comment_lines = [] + while line and "}" not in line: + comment_lines.append(line.rstrip()) + line = handle.readline() + end_index = line.find("}") + comment_lines.append(line[:end_index]) + if "}" in line: + line = line[end_index:] + else: + line = "" + + if not skip_variation_depth: + visitor.visit_comment("\n".join(comment_lines).strip()) + + # Continue with the current or the next line. + if line: + read_next_line = False + break + elif token == "(": + if skip_variation_depth: + skip_variation_depth += 1 + elif board_stack[-1].move_stack: + if visitor.begin_variation() is SKIP: + skip_variation_depth = 1 + else: + board = board_stack[-1].copy() + board.pop() + board_stack.append(board) + elif token == ")": + if skip_variation_depth == 1: + skip_variation_depth = 0 + visitor.end_variation() + elif skip_variation_depth: + skip_variation_depth -= 1 + elif len(board_stack) > 1: + visitor.end_variation() + board_stack.pop() + elif skip_variation_depth: + continue + elif token.startswith(";"): + break + elif token.startswith("$"): + # Found a NAG. + visitor.visit_nag(int(token[1:])) + elif token == "?": + visitor.visit_nag(NAG_MISTAKE) + elif token == "??": + visitor.visit_nag(NAG_BLUNDER) + elif token == "!": + visitor.visit_nag(NAG_GOOD_MOVE) + elif token == "!!": + visitor.visit_nag(NAG_BRILLIANT_MOVE) + elif token == "!?": + visitor.visit_nag(NAG_SPECULATIVE_MOVE) + elif token == "?!": + visitor.visit_nag(NAG_DUBIOUS_MOVE) + elif token in ["1-0", "0-1", "1/2-1/2", "*"] and len(board_stack) == 1: + 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) + visitor.visit_board(board_stack[-1]) + + if read_next_line: + line = handle.readline() + + visitor.end_game() + return visitor.result() + + +def read_headers(handle: TextIO) -> Optional[Headers]: + """ + Reads game headers from a PGN file opened in text mode. Skips the rest of + the game. + + Since actually parsing many games from a big file is relatively expensive, + this is a better way to look only for specific games and then seek and + parse them later. + + This example scans for the first game with Kasparov as the white player. + + >>> import chess.pgn + >>> + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn") + >>> + >>> kasparov_offsets = [] + >>> + >>> while True: + ... offset = pgn.tell() + ... + ... headers = chess.pgn.read_headers(pgn) + ... if headers is None: + ... break + ... + ... if "Kasparov" in headers.get("White", "?"): + ... kasparov_offsets.append(offset) + + Then it can later be seeked and parsed. + + >>> for offset in kasparov_offsets: + ... pgn.seek(offset) + ... chess.pgn.read_game(pgn) # doctest: +ELLIPSIS + 0 + + 1436 + + 3067 + + """ + return read_game(handle, Visitor=HeadersBuilder) + + +def skip_game(handle: TextIO) -> bool: + """ + Skips a game. Returns ``True`` if a game was found and skipped. + """ + return bool(read_game(handle, Visitor=SkipVisitor)) diff --git a/cartesi-python-chess-cartesi-img/chess/pgn.pyc b/cartesi-python-chess-cartesi-img/chess/pgn.pyc new file mode 100644 index 0000000..90a2f79 Binary files /dev/null and b/cartesi-python-chess-cartesi-img/chess/pgn.pyc differ diff --git a/cartesi-python-chess-cartesi-img/chess/polyglot.py b/cartesi-python-chess-cartesi-img/chess/polyglot.py new file mode 100644 index 0000000..1562d53 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/polyglot.py @@ -0,0 +1,542 @@ +# This file is part of the python-chess library. +# Copyright (C) 2012-2021 Niklas Fiekas +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import annotations + +import chess +import struct +import os +import mmap +import random +import typing + +from types import TracebackType +from typing import Callable, Container, Iterator, List, NamedTuple, Optional, Type, Union + + +PathLike = Union[str, bytes, os.PathLike] + + +ENTRY_STRUCT = struct.Struct(">QHHI") + + +POLYGLOT_RANDOM_ARRAY = [ + 0x9D39247E33776D41, 0x2AF7398005AAA5C7, 0x44DB015024623547, 0x9C15F73E62A76AE2, + 0x75834465489C0C89, 0x3290AC3A203001BF, 0x0FBBAD1F61042279, 0xE83A908FF2FB60CA, + 0x0D7E765D58755C10, 0x1A083822CEAFE02D, 0x9605D5F0E25EC3B0, 0xD021FF5CD13A2ED5, + 0x40BDF15D4A672E32, 0x011355146FD56395, 0x5DB4832046F3D9E5, 0x239F8B2D7FF719CC, + 0x05D1A1AE85B49AA1, 0x679F848F6E8FC971, 0x7449BBFF801FED0B, 0x7D11CDB1C3B7ADF0, + 0x82C7709E781EB7CC, 0xF3218F1C9510786C, 0x331478F3AF51BBE6, 0x4BB38DE5E7219443, + 0xAA649C6EBCFD50FC, 0x8DBD98A352AFD40B, 0x87D2074B81D79217, 0x19F3C751D3E92AE1, + 0xB4AB30F062B19ABF, 0x7B0500AC42047AC4, 0xC9452CA81A09D85D, 0x24AA6C514DA27500, + 0x4C9F34427501B447, 0x14A68FD73C910841, 0xA71B9B83461CBD93, 0x03488B95B0F1850F, + 0x637B2B34FF93C040, 0x09D1BC9A3DD90A94, 0x3575668334A1DD3B, 0x735E2B97A4C45A23, + 0x18727070F1BD400B, 0x1FCBACD259BF02E7, 0xD310A7C2CE9B6555, 0xBF983FE0FE5D8244, + 0x9F74D14F7454A824, 0x51EBDC4AB9BA3035, 0x5C82C505DB9AB0FA, 0xFCF7FE8A3430B241, + 0x3253A729B9BA3DDE, 0x8C74C368081B3075, 0xB9BC6C87167C33E7, 0x7EF48F2B83024E20, + 0x11D505D4C351BD7F, 0x6568FCA92C76A243, 0x4DE0B0F40F32A7B8, 0x96D693460CC37E5D, + 0x42E240CB63689F2F, 0x6D2BDCDAE2919661, 0x42880B0236E4D951, 0x5F0F4A5898171BB6, + 0x39F890F579F92F88, 0x93C5B5F47356388B, 0x63DC359D8D231B78, 0xEC16CA8AEA98AD76, + 0x5355F900C2A82DC7, 0x07FB9F855A997142, 0x5093417AA8A7ED5E, 0x7BCBC38DA25A7F3C, + 0x19FC8A768CF4B6D4, 0x637A7780DECFC0D9, 0x8249A47AEE0E41F7, 0x79AD695501E7D1E8, + 0x14ACBAF4777D5776, 0xF145B6BECCDEA195, 0xDABF2AC8201752FC, 0x24C3C94DF9C8D3F6, + 0xBB6E2924F03912EA, 0x0CE26C0B95C980D9, 0xA49CD132BFBF7CC4, 0xE99D662AF4243939, + 0x27E6AD7891165C3F, 0x8535F040B9744FF1, 0x54B3F4FA5F40D873, 0x72B12C32127FED2B, + 0xEE954D3C7B411F47, 0x9A85AC909A24EAA1, 0x70AC4CD9F04F21F5, 0xF9B89D3E99A075C2, + 0x87B3E2B2B5C907B1, 0xA366E5B8C54F48B8, 0xAE4A9346CC3F7CF2, 0x1920C04D47267BBD, + 0x87BF02C6B49E2AE9, 0x092237AC237F3859, 0xFF07F64EF8ED14D0, 0x8DE8DCA9F03CC54E, + 0x9C1633264DB49C89, 0xB3F22C3D0B0B38ED, 0x390E5FB44D01144B, 0x5BFEA5B4712768E9, + 0x1E1032911FA78984, 0x9A74ACB964E78CB3, 0x4F80F7A035DAFB04, 0x6304D09A0B3738C4, + 0x2171E64683023A08, 0x5B9B63EB9CEFF80C, 0x506AACF489889342, 0x1881AFC9A3A701D6, + 0x6503080440750644, 0xDFD395339CDBF4A7, 0xEF927DBCF00C20F2, 0x7B32F7D1E03680EC, + 0xB9FD7620E7316243, 0x05A7E8A57DB91B77, 0xB5889C6E15630A75, 0x4A750A09CE9573F7, + 0xCF464CEC899A2F8A, 0xF538639CE705B824, 0x3C79A0FF5580EF7F, 0xEDE6C87F8477609D, + 0x799E81F05BC93F31, 0x86536B8CF3428A8C, 0x97D7374C60087B73, 0xA246637CFF328532, + 0x043FCAE60CC0EBA0, 0x920E449535DD359E, 0x70EB093B15B290CC, 0x73A1921916591CBD, + 0x56436C9FE1A1AA8D, 0xEFAC4B70633B8F81, 0xBB215798D45DF7AF, 0x45F20042F24F1768, + 0x930F80F4E8EB7462, 0xFF6712FFCFD75EA1, 0xAE623FD67468AA70, 0xDD2C5BC84BC8D8FC, + 0x7EED120D54CF2DD9, 0x22FE545401165F1C, 0xC91800E98FB99929, 0x808BD68E6AC10365, + 0xDEC468145B7605F6, 0x1BEDE3A3AEF53302, 0x43539603D6C55602, 0xAA969B5C691CCB7A, + 0xA87832D392EFEE56, 0x65942C7B3C7E11AE, 0xDED2D633CAD004F6, 0x21F08570F420E565, + 0xB415938D7DA94E3C, 0x91B859E59ECB6350, 0x10CFF333E0ED804A, 0x28AED140BE0BB7DD, + 0xC5CC1D89724FA456, 0x5648F680F11A2741, 0x2D255069F0B7DAB3, 0x9BC5A38EF729ABD4, + 0xEF2F054308F6A2BC, 0xAF2042F5CC5C2858, 0x480412BAB7F5BE2A, 0xAEF3AF4A563DFE43, + 0x19AFE59AE451497F, 0x52593803DFF1E840, 0xF4F076E65F2CE6F0, 0x11379625747D5AF3, + 0xBCE5D2248682C115, 0x9DA4243DE836994F, 0x066F70B33FE09017, 0x4DC4DE189B671A1C, + 0x51039AB7712457C3, 0xC07A3F80C31FB4B4, 0xB46EE9C5E64A6E7C, 0xB3819A42ABE61C87, + 0x21A007933A522A20, 0x2DF16F761598AA4F, 0x763C4A1371B368FD, 0xF793C46702E086A0, + 0xD7288E012AEB8D31, 0xDE336A2A4BC1C44B, 0x0BF692B38D079F23, 0x2C604A7A177326B3, + 0x4850E73E03EB6064, 0xCFC447F1E53C8E1B, 0xB05CA3F564268D99, 0x9AE182C8BC9474E8, + 0xA4FC4BD4FC5558CA, 0xE755178D58FC4E76, 0x69B97DB1A4C03DFE, 0xF9B5B7C4ACC67C96, + 0xFC6A82D64B8655FB, 0x9C684CB6C4D24417, 0x8EC97D2917456ED0, 0x6703DF9D2924E97E, + 0xC547F57E42A7444E, 0x78E37644E7CAD29E, 0xFE9A44E9362F05FA, 0x08BD35CC38336615, + 0x9315E5EB3A129ACE, 0x94061B871E04DF75, 0xDF1D9F9D784BA010, 0x3BBA57B68871B59D, + 0xD2B7ADEEDED1F73F, 0xF7A255D83BC373F8, 0xD7F4F2448C0CEB81, 0xD95BE88CD210FFA7, + 0x336F52F8FF4728E7, 0xA74049DAC312AC71, 0xA2F61BB6E437FDB5, 0x4F2A5CB07F6A35B3, + 0x87D380BDA5BF7859, 0x16B9F7E06C453A21, 0x7BA2484C8A0FD54E, 0xF3A678CAD9A2E38C, + 0x39B0BF7DDE437BA2, 0xFCAF55C1BF8A4424, 0x18FCF680573FA594, 0x4C0563B89F495AC3, + 0x40E087931A00930D, 0x8CFFA9412EB642C1, 0x68CA39053261169F, 0x7A1EE967D27579E2, + 0x9D1D60E5076F5B6F, 0x3810E399B6F65BA2, 0x32095B6D4AB5F9B1, 0x35CAB62109DD038A, + 0xA90B24499FCFAFB1, 0x77A225A07CC2C6BD, 0x513E5E634C70E331, 0x4361C0CA3F692F12, + 0xD941ACA44B20A45B, 0x528F7C8602C5807B, 0x52AB92BEB9613989, 0x9D1DFA2EFC557F73, + 0x722FF175F572C348, 0x1D1260A51107FE97, 0x7A249A57EC0C9BA2, 0x04208FE9E8F7F2D6, + 0x5A110C6058B920A0, 0x0CD9A497658A5698, 0x56FD23C8F9715A4C, 0x284C847B9D887AAE, + 0x04FEABFBBDB619CB, 0x742E1E651C60BA83, 0x9A9632E65904AD3C, 0x881B82A13B51B9E2, + 0x506E6744CD974924, 0xB0183DB56FFC6A79, 0x0ED9B915C66ED37E, 0x5E11E86D5873D484, + 0xF678647E3519AC6E, 0x1B85D488D0F20CC5, 0xDAB9FE6525D89021, 0x0D151D86ADB73615, + 0xA865A54EDCC0F019, 0x93C42566AEF98FFB, 0x99E7AFEABE000731, 0x48CBFF086DDF285A, + 0x7F9B6AF1EBF78BAF, 0x58627E1A149BBA21, 0x2CD16E2ABD791E33, 0xD363EFF5F0977996, + 0x0CE2A38C344A6EED, 0x1A804AADB9CFA741, 0x907F30421D78C5DE, 0x501F65EDB3034D07, + 0x37624AE5A48FA6E9, 0x957BAF61700CFF4E, 0x3A6C27934E31188A, 0xD49503536ABCA345, + 0x088E049589C432E0, 0xF943AEE7FEBF21B8, 0x6C3B8E3E336139D3, 0x364F6FFA464EE52E, + 0xD60F6DCEDC314222, 0x56963B0DCA418FC0, 0x16F50EDF91E513AF, 0xEF1955914B609F93, + 0x565601C0364E3228, 0xECB53939887E8175, 0xBAC7A9A18531294B, 0xB344C470397BBA52, + 0x65D34954DAF3CEBD, 0xB4B81B3FA97511E2, 0xB422061193D6F6A7, 0x071582401C38434D, + 0x7A13F18BBEDC4FF5, 0xBC4097B116C524D2, 0x59B97885E2F2EA28, 0x99170A5DC3115544, + 0x6F423357E7C6A9F9, 0x325928EE6E6F8794, 0xD0E4366228B03343, 0x565C31F7DE89EA27, + 0x30F5611484119414, 0xD873DB391292ED4F, 0x7BD94E1D8E17DEBC, 0xC7D9F16864A76E94, + 0x947AE053EE56E63C, 0xC8C93882F9475F5F, 0x3A9BF55BA91F81CA, 0xD9A11FBB3D9808E4, + 0x0FD22063EDC29FCA, 0xB3F256D8ACA0B0B9, 0xB03031A8B4516E84, 0x35DD37D5871448AF, + 0xE9F6082B05542E4E, 0xEBFAFA33D7254B59, 0x9255ABB50D532280, 0xB9AB4CE57F2D34F3, + 0x693501D628297551, 0xC62C58F97DD949BF, 0xCD454F8F19C5126A, 0xBBE83F4ECC2BDECB, + 0xDC842B7E2819E230, 0xBA89142E007503B8, 0xA3BC941D0A5061CB, 0xE9F6760E32CD8021, + 0x09C7E552BC76492F, 0x852F54934DA55CC9, 0x8107FCCF064FCF56, 0x098954D51FFF6580, + 0x23B70EDB1955C4BF, 0xC330DE426430F69D, 0x4715ED43E8A45C0A, 0xA8D7E4DAB780A08D, + 0x0572B974F03CE0BB, 0xB57D2E985E1419C7, 0xE8D9ECBE2CF3D73F, 0x2FE4B17170E59750, + 0x11317BA87905E790, 0x7FBF21EC8A1F45EC, 0x1725CABFCB045B00, 0x964E915CD5E2B207, + 0x3E2B8BCBF016D66D, 0xBE7444E39328A0AC, 0xF85B2B4FBCDE44B7, 0x49353FEA39BA63B1, + 0x1DD01AAFCD53486A, 0x1FCA8A92FD719F85, 0xFC7C95D827357AFA, 0x18A6A990C8B35EBD, + 0xCCCB7005C6B9C28D, 0x3BDBB92C43B17F26, 0xAA70B5B4F89695A2, 0xE94C39A54A98307F, + 0xB7A0B174CFF6F36E, 0xD4DBA84729AF48AD, 0x2E18BC1AD9704A68, 0x2DE0966DAF2F8B1C, + 0xB9C11D5B1E43A07E, 0x64972D68DEE33360, 0x94628D38D0C20584, 0xDBC0D2B6AB90A559, + 0xD2733C4335C6A72F, 0x7E75D99D94A70F4D, 0x6CED1983376FA72B, 0x97FCAACBF030BC24, + 0x7B77497B32503B12, 0x8547EDDFB81CCB94, 0x79999CDFF70902CB, 0xCFFE1939438E9B24, + 0x829626E3892D95D7, 0x92FAE24291F2B3F1, 0x63E22C147B9C3403, 0xC678B6D860284A1C, + 0x5873888850659AE7, 0x0981DCD296A8736D, 0x9F65789A6509A440, 0x9FF38FED72E9052F, + 0xE479EE5B9930578C, 0xE7F28ECD2D49EECD, 0x56C074A581EA17FE, 0x5544F7D774B14AEF, + 0x7B3F0195FC6F290F, 0x12153635B2C0CF57, 0x7F5126DBBA5E0CA7, 0x7A76956C3EAFB413, + 0x3D5774A11D31AB39, 0x8A1B083821F40CB4, 0x7B4A38E32537DF62, 0x950113646D1D6E03, + 0x4DA8979A0041E8A9, 0x3BC36E078F7515D7, 0x5D0A12F27AD310D1, 0x7F9D1A2E1EBE1327, + 0xDA3A361B1C5157B1, 0xDCDD7D20903D0C25, 0x36833336D068F707, 0xCE68341F79893389, + 0xAB9090168DD05F34, 0x43954B3252DC25E5, 0xB438C2B67F98E5E9, 0x10DCD78E3851A492, + 0xDBC27AB5447822BF, 0x9B3CDB65F82CA382, 0xB67B7896167B4C84, 0xBFCED1B0048EAC50, + 0xA9119B60369FFEBD, 0x1FFF7AC80904BF45, 0xAC12FB171817EEE7, 0xAF08DA9177DDA93D, + 0x1B0CAB936E65C744, 0xB559EB1D04E5E932, 0xC37B45B3F8D6F2BA, 0xC3A9DC228CAAC9E9, + 0xF3B8B6675A6507FF, 0x9FC477DE4ED681DA, 0x67378D8ECCEF96CB, 0x6DD856D94D259236, + 0xA319CE15B0B4DB31, 0x073973751F12DD5E, 0x8A8E849EB32781A5, 0xE1925C71285279F5, + 0x74C04BF1790C0EFE, 0x4DDA48153C94938A, 0x9D266D6A1CC0542C, 0x7440FB816508C4FE, + 0x13328503DF48229F, 0xD6BF7BAEE43CAC40, 0x4838D65F6EF6748F, 0x1E152328F3318DEA, + 0x8F8419A348F296BF, 0x72C8834A5957B511, 0xD7A023A73260B45C, 0x94EBC8ABCFB56DAE, + 0x9FC10D0F989993E0, 0xDE68A2355B93CAE6, 0xA44CFE79AE538BBE, 0x9D1D84FCCE371425, + 0x51D2B1AB2DDFB636, 0x2FD7E4B9E72CD38C, 0x65CA5B96B7552210, 0xDD69A0D8AB3B546D, + 0x604D51B25FBF70E2, 0x73AA8A564FB7AC9E, 0x1A8C1E992B941148, 0xAAC40A2703D9BEA0, + 0x764DBEAE7FA4F3A6, 0x1E99B96E70A9BE8B, 0x2C5E9DEB57EF4743, 0x3A938FEE32D29981, + 0x26E6DB8FFDF5ADFE, 0x469356C504EC9F9D, 0xC8763C5B08D1908C, 0x3F6C6AF859D80055, + 0x7F7CC39420A3A545, 0x9BFB227EBDF4C5CE, 0x89039D79D6FC5C5C, 0x8FE88B57305E2AB6, + 0xA09E8C8C35AB96DE, 0xFA7E393983325753, 0xD6B6D0ECC617C699, 0xDFEA21EA9E7557E3, + 0xB67C1FA481680AF8, 0xCA1E3785A9E724E5, 0x1CFC8BED0D681639, 0xD18D8549D140CAEA, + 0x4ED0FE7E9DC91335, 0xE4DBF0634473F5D2, 0x1761F93A44D5AEFE, 0x53898E4C3910DA55, + 0x734DE8181F6EC39A, 0x2680B122BAA28D97, 0x298AF231C85BAFAB, 0x7983EED3740847D5, + 0x66C1A2A1A60CD889, 0x9E17E49642A3E4C1, 0xEDB454E7BADC0805, 0x50B704CAB602C329, + 0x4CC317FB9CDDD023, 0x66B4835D9EAFEA22, 0x219B97E26FFC81BD, 0x261E4E4C0A333A9D, + 0x1FE2CCA76517DB90, 0xD7504DFA8816EDBB, 0xB9571FA04DC089C8, 0x1DDC0325259B27DE, + 0xCF3F4688801EB9AA, 0xF4F5D05C10CAB243, 0x38B6525C21A42B0E, 0x36F60E2BA4FA6800, + 0xEB3593803173E0CE, 0x9C4CD6257C5A3603, 0xAF0C317D32ADAA8A, 0x258E5A80C7204C4B, + 0x8B889D624D44885D, 0xF4D14597E660F855, 0xD4347F66EC8941C3, 0xE699ED85B0DFB40D, + 0x2472F6207C2D0484, 0xC2A1E7B5B459AEB5, 0xAB4F6451CC1D45EC, 0x63767572AE3D6174, + 0xA59E0BD101731A28, 0x116D0016CB948F09, 0x2CF9C8CA052F6E9F, 0x0B090A7560A968E3, + 0xABEEDDB2DDE06FF1, 0x58EFC10B06A2068D, 0xC6E57A78FBD986E0, 0x2EAB8CA63CE802D7, + 0x14A195640116F336, 0x7C0828DD624EC390, 0xD74BBE77E6116AC7, 0x804456AF10F5FB53, + 0xEBE9EA2ADF4321C7, 0x03219A39EE587A30, 0x49787FEF17AF9924, 0xA1E9300CD8520548, + 0x5B45E522E4B1B4EF, 0xB49C3B3995091A36, 0xD4490AD526F14431, 0x12A8F216AF9418C2, + 0x001F837CC7350524, 0x1877B51E57A764D5, 0xA2853B80F17F58EE, 0x993E1DE72D36D310, + 0xB3598080CE64A656, 0x252F59CF0D9F04BB, 0xD23C8E176D113600, 0x1BDA0492E7E4586E, + 0x21E0BD5026C619BF, 0x3B097ADAF088F94E, 0x8D14DEDB30BE846E, 0xF95CFFA23AF5F6F4, + 0x3871700761B3F743, 0xCA672B91E9E4FA16, 0x64C8E531BFF53B55, 0x241260ED4AD1E87D, + 0x106C09B972D2E822, 0x7FBA195410E5CA30, 0x7884D9BC6CB569D8, 0x0647DFEDCD894A29, + 0x63573FF03E224774, 0x4FC8E9560F91B123, 0x1DB956E450275779, 0xB8D91274B9E9D4FB, + 0xA2EBEE47E2FBFCE1, 0xD9F1F30CCD97FB09, 0xEFED53D75FD64E6B, 0x2E6D02C36017F67F, + 0xA9AA4D20DB084E9B, 0xB64BE8D8B25396C1, 0x70CB6AF7C2D5BCF0, 0x98F076A4F7A2322E, + 0xBF84470805E69B5F, 0x94C3251F06F90CF3, 0x3E003E616A6591E9, 0xB925A6CD0421AFF3, + 0x61BDD1307C66E300, 0xBF8D5108E27E0D48, 0x240AB57A8B888B20, 0xFC87614BAF287E07, + 0xEF02CDD06FFDB432, 0xA1082C0466DF6C0A, 0x8215E577001332C8, 0xD39BB9C3A48DB6CF, + 0x2738259634305C14, 0x61CF4F94C97DF93D, 0x1B6BACA2AE4E125B, 0x758F450C88572E0B, + 0x959F587D507A8359, 0xB063E962E045F54D, 0x60E8ED72C0DFF5D1, 0x7B64978555326F9F, + 0xFD080D236DA814BA, 0x8C90FD9B083F4558, 0x106F72FE81E2C590, 0x7976033A39F7D952, + 0xA4EC0132764CA04B, 0x733EA705FAE4FA77, 0xB4D8F77BC3E56167, 0x9E21F4F903B33FD9, + 0x9D765E419FB69F6D, 0xD30C088BA61EA5EF, 0x5D94337FBFAF7F5B, 0x1A4E4822EB4D7A59, + 0x6FFE73E81B637FB3, 0xDDF957BC36D8B9CA, 0x64D0E29EEA8838B3, 0x08DD9BDFD96B9F63, + 0x087E79E5A57D1D13, 0xE328E230E3E2B3FB, 0x1C2559E30F0946BE, 0x720BF5F26F4D2EAA, + 0xB0774D261CC609DB, 0x443F64EC5A371195, 0x4112CF68649A260E, 0xD813F2FAB7F5C5CA, + 0x660D3257380841EE, 0x59AC2C7873F910A3, 0xE846963877671A17, 0x93B633ABFA3469F8, + 0xC0C0F5A60EF4CDCF, 0xCAF21ECD4377B28C, 0x57277707199B8175, 0x506C11B9D90E8B1D, + 0xD83CC2687A19255F, 0x4A29C6465A314CD1, 0xED2DF21216235097, 0xB5635C95FF7296E2, + 0x22AF003AB672E811, 0x52E762596BF68235, 0x9AEBA33AC6ECC6B0, 0x944F6DE09134DFB6, + 0x6C47BEC883A7DE39, 0x6AD047C430A12104, 0xA5B1CFDBA0AB4067, 0x7C45D833AFF07862, + 0x5092EF950A16DA0B, 0x9338E69C052B8E7B, 0x455A4B4CFE30E3F5, 0x6B02E63195AD0CF8, + 0x6B17B224BAD6BF27, 0xD1E0CCD25BB9C169, 0xDE0C89A556B9AE70, 0x50065E535A213CF6, + 0x9C1169FA2777B874, 0x78EDEFD694AF1EED, 0x6DC93D9526A50E68, 0xEE97F453F06791ED, + 0x32AB0EDB696703D3, 0x3A6853C7E70757A7, 0x31865CED6120F37D, 0x67FEF95D92607890, + 0x1F2B1D1F15F6DC9C, 0xB69E38A8965C6B65, 0xAA9119FF184CCCF4, 0xF43C732873F24C13, + 0xFB4A3D794A9A80D2, 0x3550C2321FD6109C, 0x371F77E76BB8417E, 0x6BFA9AAE5EC05779, + 0xCD04F3FF001A4778, 0xE3273522064480CA, 0x9F91508BFFCFC14A, 0x049A7F41061A9E60, + 0xFCB6BE43A9F2FE9B, 0x08DE8A1C7797DA9B, 0x8F9887E6078735A1, 0xB5B4071DBFC73A66, + 0x230E343DFBA08D33, 0x43ED7F5A0FAE657D, 0x3A88A0FBBCB05C63, 0x21874B8B4D2DBC4F, + 0x1BDEA12E35F6A8C9, 0x53C065C6C8E63528, 0xE34A1D250E7A8D6B, 0xD6B04D3B7651DD7E, + 0x5E90277E7CB39E2D, 0x2C046F22062DC67D, 0xB10BB459132D0A26, 0x3FA9DDFB67E2F199, + 0x0E09B88E1914F7AF, 0x10E8B35AF3EEAB37, 0x9EEDECA8E272B933, 0xD4C718BC4AE8AE5F, + 0x81536D601170FC20, 0x91B534F885818A06, 0xEC8177F83F900978, 0x190E714FADA5156E, + 0xB592BF39B0364963, 0x89C350C893AE7DC1, 0xAC042E70F8B383F2, 0xB49B52E587A1EE60, + 0xFB152FE3FF26DA89, 0x3E666E6F69AE2C15, 0x3B544EBE544C19F9, 0xE805A1E290CF2456, + 0x24B33C9D7ED25117, 0xE74733427B72F0C1, 0x0A804D18B7097475, 0x57E3306D881EDB4F, + 0x4AE7D6A36EB5DBCB, 0x2D8D5432157064C8, 0xD1E649DE1E7F268B, 0x8A328A1CEDFE552C, + 0x07A3AEC79624C7DA, 0x84547DDC3E203C94, 0x990A98FD5071D263, 0x1A4FF12616EEFC89, + 0xF6F7FD1431714200, 0x30C05B1BA332F41C, 0x8D2636B81555A786, 0x46C9FEB55D120902, + 0xCCEC0A73B49C9921, 0x4E9D2827355FC492, 0x19EBB029435DCB0F, 0x4659D2B743848A2C, + 0x963EF2C96B33BE31, 0x74F85198B05A2E7D, 0x5A0F544DD2B1FB18, 0x03727073C2E134B1, + 0xC7F6AA2DE59AEA61, 0x352787BAA0D7C22F, 0x9853EAB63B5E0B35, 0xABBDCDD7ED5C0860, + 0xCF05DAF5AC8D77B0, 0x49CAD48CEBF4A71E, 0x7A4C10EC2158C4A6, 0xD9E92AA246BF719E, + 0x13AE978D09FE5557, 0x730499AF921549FF, 0x4E4B705B92903BA4, 0xFF577222C14F0A3A, + 0x55B6344CF97AAFAE, 0xB862225B055B6960, 0xCAC09AFBDDD2CDB4, 0xDAF8E9829FE96B5F, + 0xB5FDFC5D3132C498, 0x310CB380DB6F7503, 0xE87FBB46217A360E, 0x2102AE466EBB1148, + 0xF8549E1A3AA5E00D, 0x07A69AFDCC42261A, 0xC4C118BFE78FEAAE, 0xF9F4892ED96BD438, + 0x1AF3DBE25D8F45DA, 0xF5B4B0B0D2DEEEB4, 0x962ACEEFA82E1C84, 0x046E3ECAAF453CE9, + 0xF05D129681949A4C, 0x964781CE734B3C84, 0x9C2ED44081CE5FBD, 0x522E23F3925E319E, + 0x177E00F9FC32F791, 0x2BC60A63A6F3B3F2, 0x222BBFAE61725606, 0x486289DDCC3D6780, + 0x7DC7785B8EFDFC80, 0x8AF38731C02BA980, 0x1FAB64EA29A2DDF7, 0xE4D9429322CD065A, + 0x9DA058C67844F20C, 0x24C0E332B70019B0, 0x233003B5A6CFE6AD, 0xD586BD01C5C217F6, + 0x5E5637885F29BC2B, 0x7EBA726D8C94094B, 0x0A56A5F0BFE39272, 0xD79476A84EE20D06, + 0x9E4C1269BAA4BF37, 0x17EFEE45B0DEE640, 0x1D95B0A5FCF90BC6, 0x93CBE0B699C2585D, + 0x65FA4F227A2B6D79, 0xD5F9E858292504D5, 0xC2B5A03F71471A6F, 0x59300222B4561E00, + 0xCE2F8642CA0712DC, 0x7CA9723FBB2E8988, 0x2785338347F2BA08, 0xC61BB3A141E50E8C, + 0x150F361DAB9DEC26, 0x9F6A419D382595F4, 0x64A53DC924FE7AC9, 0x142DE49FFF7A7C3D, + 0x0C335248857FA9E7, 0x0A9C32D5EAE45305, 0xE6C42178C4BBB92E, 0x71F1CE2490D20B07, + 0xF1BCC3D275AFE51A, 0xE728E8C83C334074, 0x96FBF83A12884624, 0x81A1549FD6573DA5, + 0x5FA7867CAF35E149, 0x56986E2EF3ED091B, 0x917F1DD5F8886C61, 0xD20D8C88C8FFE65F, + 0x31D71DCE64B2C310, 0xF165B587DF898190, 0xA57E6339DD2CF3A0, 0x1EF6E6DBB1961EC9, + 0x70CC73D90BC26E24, 0xE21A6B35DF0C3AD7, 0x003A93D8B2806962, 0x1C99DED33CB890A1, + 0xCF3145DE0ADD4289, 0xD0E4427A5514FB72, 0x77C621CC9FB3A483, 0x67A34DAC4356550B, + 0xF8D626AAAF278509 +] + + +class ZobristHasher: + def __init__(self, array: List[int]) -> None: + assert len(array) >= 781 + self.array = array + + def hash_board(self, board: chess.BaseBoard) -> int: + zobrist_hash = 0 + + for pivot, squares in enumerate(board.occupied_co): + for square in chess.scan_reversed(squares): + piece_index = (typing.cast(chess.PieceType, board.piece_type_at(square)) - 1) * 2 + pivot + zobrist_hash ^= self.array[64 * piece_index + square] + + return zobrist_hash + + def hash_castling(self, board: chess.Board) -> int: + zobrist_hash = 0 + + # Hash in the castling flags. + if board.has_kingside_castling_rights(chess.WHITE): + zobrist_hash ^= self.array[768] + if board.has_queenside_castling_rights(chess.WHITE): + zobrist_hash ^= self.array[768 + 1] + if board.has_kingside_castling_rights(chess.BLACK): + zobrist_hash ^= self.array[768 + 2] + if board.has_queenside_castling_rights(chess.BLACK): + zobrist_hash ^= self.array[768 + 3] + + return zobrist_hash + + def hash_ep_square(self, board: chess.Board) -> int: + # Hash in the en passant file. + if board.ep_square: + # But only if there's actually a pawn ready to capture it. Legality + # of the potential capture is irrelevant. + if board.turn == chess.WHITE: + ep_mask = chess.shift_down(chess.BB_SQUARES[board.ep_square]) + else: + ep_mask = chess.shift_up(chess.BB_SQUARES[board.ep_square]) + ep_mask = chess.shift_left(ep_mask) | chess.shift_right(ep_mask) + + if ep_mask & board.pawns & board.occupied_co[board.turn]: + return self.array[772 + chess.square_file(board.ep_square)] + return 0 + + def hash_turn(self, board: chess.Board) -> int: + # Hash in the turn. + return self.array[780] if board.turn == chess.WHITE else 0 + + def __call__(self, board: chess.Board) -> int: + return (self.hash_board(board) ^ self.hash_castling(board) ^ + self.hash_ep_square(board) ^ self.hash_turn(board)) + + +def zobrist_hash(board: chess.Board, *, _hasher: Callable[[chess.Board], int] = ZobristHasher(POLYGLOT_RANDOM_ARRAY)) -> int: + """ + Calculates the Polyglot Zobrist hash of the position. + + A Zobrist hash is an XOR of pseudo-random values picked from + an array. Which values are picked is decided by features of the + position, such as piece positions, castling rights and en passant + squares. + """ + return _hasher(board) + + +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): + def size(self) -> int: + return 0 + + def close(self) -> 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) + + try: + self.mmap: Union[mmap.mmap, _EmptyMmap] = mmap.mmap(self.fd, 0, access=mmap.ACCESS_READ) + except (ValueError, OSError): + self.mmap = _EmptyMmap() # Workaround for empty opening books. + + 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 + except AttributeError: + pass + + def __enter__(self) -> MemoryMappedReader: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + return self.close() + + def __len__(self) -> int: + return self.mmap.size() // ENTRY_STRUCT.size + + def __getitem__(self, index: int) -> Entry: + if index < 0: + index = len(self) + index + + try: + key, raw_move, weight, learn = ENTRY_STRUCT.unpack_from(self.mmap, index * ENTRY_STRUCT.size) + except struct.error: + raise IndexError() + + # Extract source and target square. + to_square = raw_move & 0x3f + from_square = (raw_move >> 6) & 0x3f + + # Extract the promotion type. + promotion_part = (raw_move >> 12) & 0x7 + promotion = promotion_part + 1 if promotion_part else None + + # Piece drop. + if from_square == to_square: + promotion, drop = None, promotion + else: + drop = None + + # Entry with move (not normalized). + move = chess.Move(from_square, to_square, promotion, drop) + return Entry(key, raw_move, weight, learn, move) + + def __iter__(self) -> Iterator[Entry]: + i = 0 + size = len(self) + while i < size: + yield self[i] + i += 1 + + def bisect_key_left(self, key: int) -> int: + lo = 0 + hi = len(self) + + while lo < hi: + mid = (lo + hi) // 2 + mid_key, _, _, _ = ENTRY_STRUCT.unpack_from(self.mmap, mid * ENTRY_STRUCT.size) + if mid_key < key: + lo = mid + 1 + else: + hi = mid + + return lo + + 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]: + """Seeks a specific position and yields corresponding entries.""" + try: + key = int(board) # type: ignore + context: Optional[chess.Board] = None + except (TypeError, ValueError): + context = typing.cast(chess.Board, board) + key = zobrist_hash(context) + + i = self.bisect_key_left(key) + size = len(self) + + while i < size: + entry = self[i] + i += 1 + + if entry.key != key: + break + + if entry.weight < minimum_weight: + continue + + if context: + move = context._from_chess960(context.chess960, entry.move.from_square, entry.move.to_square, entry.move.promotion, entry.move.drop) + entry = Entry(entry.key, entry.raw_move, entry.weight, entry.learn, move) + + if exclude_moves and entry.move in exclude_moves: + continue + + if context and not context.is_legal(entry.move): + continue + + yield 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. + + The main entry is the (first) entry with the highest weight. + + By default, entries with weight ``0`` are excluded. This is a common + way to delete entries from an opening book without compacting it. Pass + *minimum_weight* ``0`` to select all entries. + + :raises: :exc:`IndexError` if no entries are found. Use + :func:`~chess.polyglot.MemoryMappedReader.get()` if you prefer to + get ``None`` instead of an exception. + """ + try: + return max(self.find_all(board, minimum_weight=minimum_weight, exclude_moves=exclude_moves), key=lambda entry: entry.weight) + 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]: + try: + return self.find(board, minimum_weight=minimum_weight, exclude_moves=exclude_moves) + except IndexError: + return default + + def choice(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, exclude_moves: Container[chess.Move] = [], random: Optional[random.Random] = None) -> Entry: + """ + Uniformly selects a random entry for the given position. + + :raises: :exc:`IndexError` if no entries are found. + """ + 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 _randint(random, 0, i) == i: + chosen_entry = entry + + if chosen_entry is None: + raise IndexError() + + return chosen_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. + + :raises: :exc:`IndexError` if no entries are found. + """ + total_weights = sum(entry.weight for entry in self.find_all(board, exclude_moves=exclude_moves)) + if not total_weights: + raise IndexError() + + choice = _randint(random, 0, total_weights - 1) + + current_sum = 0 + for entry in self.find_all(board, exclude_moves=exclude_moves): + current_sum += entry.weight + if current_sum > choice: + return entry + + assert False + + def close(self) -> None: + """Closes the reader.""" + self.mmap.close() + + try: + os.close(self.fd) + except OSError: + pass + + +def open_reader(path: PathLike) -> MemoryMappedReader: + """ + Creates a reader for the file at the given path. + + The following example opens a book to find all entries for the start + position: + + >>> import chess + >>> import chess.polyglot + >>> + >>> board = chess.Board() + >>> + >>> with chess.polyglot.open_reader("data/polyglot/performance.bin") as reader: + ... for entry in reader.find_all(board): + ... print(entry.move, entry.weight, entry.learn) + e2e4 1 0 + d2d4 1 0 + c2c4 1 0 + """ + return MemoryMappedReader(path) diff --git a/cartesi-python-chess-cartesi-img/chess/py.typed b/cartesi-python-chess-cartesi-img/chess/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/cartesi-python-chess-cartesi-img/chess/svg.py b/cartesi-python-chess-cartesi-img/chess/svg.py new file mode 100644 index 0000000..9ddf044 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/svg.py @@ -0,0 +1,466 @@ +# This file is part of the python-chess library. +# Copyright (C) 2016-2021 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 math +import xml.etree.ElementTree as ET + +import chess + +from typing import Dict, Iterable, Optional, Tuple, Union +from chess import Color, IntoSquareSet, Square + + +SQUARE_SIZE = 45 +MARGIN = 20 + +PIECES = { + "b": """""", # noqa: E501 + "k": """""", # noqa: E501 + "n": """""", # noqa: E501 + "p": """""", # noqa: E501 + "q": """""", # noqa: E501 + "r": """""", # noqa: E501 + "B": """""", # noqa: E501 + "K": """""", # noqa: E501 + "N": """""", # noqa: E501 + "P": """""", # noqa: E501 + "Q": """""", # noqa: E501 + "R": """""", # noqa: E501 +} + +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 + +DEFAULT_COLORS = { + "square light": "#ffce9e", + "square dark": "#d18b47", + "square dark lastmove": "#aaa23b", + "square light lastmove": "#cdd16a", + "margin": "#212121", + "coord": "#e5e5e5", + "arrow green": "#15781B80", + "arrow red": "#88202080", + "arrow yellow": "#e68f00b3", + "arrow blue": "#00308880", +} + + +class Arrow: + """Details of an arrow to be drawn.""" + + 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: + return self + + +def _svg(viewbox: int, size: Optional[int]) -> ET.Element: + svg = ET.Element("svg", { + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "version": "1.2", + "baseProfile": "tiny", + "viewBox": f"0 0 {viewbox:d} {viewbox:d}", + }) + + if size is not None: + svg.set("width", str(size)) + svg.set("height", str(size)) + + return svg + + +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 _color(colors: Dict[str, str], color: str) -> Tuple[str, float]: + color = colors.get(color, DEFAULT_COLORS[color]) + if color.startswith("#"): + try: + if len(color) == 5: + return color[:4], int(color[4], 16) / 0xf + elif len(color) == 9: + return color[:7], int(color[7:], 16) / 0xff + except ValueError: + pass # Ignore invalid hex value + return color, 1.0 + + +def _coord(text: str, x: int, y: int, width: int, height: int, horizontal: bool, margin: int, *, color: str, opacity: float) -> ET.Element: + scale = margin / MARGIN + + if horizontal: + x += int(width - scale * width) // 2 + else: + y += int(height - scale * height) // 2 + + t = ET.Element("g", _attrs({ + "transform": f"translate({x}, {y}) scale({scale}, {scale})", + "fill": color, + "stroke": color, + "opacity": opacity if opacity < 1.0 else None, + })) + t.append(ET.fromstring(COORDS[text])) + return t + + +def piece(piece: chess.Piece, size: Optional[int] = None) -> str: + """ + Renders the given :class:`chess.Piece` as an SVG image. + + >>> import chess + >>> import chess.svg + >>> + >>> chess.svg.piece(chess.Piece.from_symbol("R")) # doctest: +SKIP + + .. image:: ../docs/wR.svg + :alt: R + """ + svg = _svg(SQUARE_SIZE, size) + svg.append(ET.fromstring(PIECES[piece.symbol()])) + return SvgWrapper(ET.tostring(svg).decode("utf-8")) + + +def board(board: Optional[chess.BaseBoard] = None, *, + orientation: Color = chess.WHITE, + lastmove: Optional[chess.Move] = None, + check: Optional[Square] = None, + arrows: Iterable[Union[Arrow, Tuple[Square, Square]]] = [], + squares: Optional[IntoSquareSet] = None, + size: Optional[int] = None, + coordinates: bool = True, + colors: Dict[str, str] = {}, + flipped: 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 + ``None`` (the default) for a chessboard without pieces. + :param orientation: The point of view, defaulting to ``chess.WHITE``. + :param lastmove: A :class:`chess.Move` to be highlighted. + :param check: A square to be marked 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 squares: A :class:`chess.SquareSet` with selected squares. + :param size: The size of the image in pixels (e.g., ``400`` for a 400 by + 400 board), or ``None`` (the default) for no size limit. + :param coordinates: Pass ``False`` to disable the coordinate margin. + :param colors: A dictionary to override default colors. Possible keys are + ``square light``, ``square dark``, ``square light lastmove``, + ``square dark lastmove``, ``margin``, ``coord``, ``arrow green``, + ``arrow blue``, ``arrow red``, and ``arrow yellow``. Values should look + like ``#ffce9e`` (opaque), or ``#15781B80`` (transparent). + :param flipped: Pass ``True`` to flip the board. + :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, squares=squares, size=350) # doctest: +SKIP + + .. image:: ../docs/Ne4.svg + :alt: 8/8/8/8/4N3/8/8/8 + + .. deprecated:: 1.1 + Use *orientation* with a color instead of the *flipped* toggle. + """ + orientation ^= flipped + margin = 15 if coordinates else 0 + svg = _svg(8 * SQUARE_SIZE + 2 * margin, size) + + if style: + ET.SubElement(svg, "style").text = style + + defs = ET.SubElement(svg, "defs") + if board: + for piece_color in chess.COLORS: + for piece_type in chess.PIECE_TYPES: + if board.pieces_mask(piece_type, piece_color): + defs.append(ET.fromstring(PIECES[chess.Piece(piece_type, piece_color).symbol()])) + + squares = chess.SquareSet(squares) if squares else chess.SquareSet() + if squares: + defs.append(ET.fromstring(XX)) + + if check is not None: + defs.append(ET.fromstring(CHECK_GRADIENT)) + + # Render coordinates. + if coordinates: + margin_color, margin_opacity = _color(colors, "margin") + ET.SubElement(svg, "rect", _attrs({ + "x": 0, + "y": 0, + "width": 2 * margin + 8 * SQUARE_SIZE, + "height": 2 * margin + 8 * SQUARE_SIZE, + "fill": margin_color, + "opacity": margin_opacity if margin_opacity < 1.0 else None, + })) + coord_color, coord_opacity = _color(colors, "coord") + for file_index, file_name in enumerate(chess.FILE_NAMES): + x = (file_index if orientation else 7 - file_index) * SQUARE_SIZE + margin + svg.append(_coord(file_name, x, 0, SQUARE_SIZE, margin, True, margin, color=coord_color, opacity=coord_opacity)) + svg.append(_coord(file_name, x, margin + 8 * SQUARE_SIZE, 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 + margin + svg.append(_coord(rank_name, 0, y, margin, SQUARE_SIZE, False, margin, color=coord_color, opacity=coord_opacity)) + svg.append(_coord(rank_name, margin + 8 * SQUARE_SIZE, 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 orientation else 7 - file_index) * SQUARE_SIZE + margin + y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + margin + + 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, fill_opacity = _color(colors, " ".join(cls)) + + cls.append(chess.SQUARE_NAMES[square]) + + ET.SubElement(svg, "rect", _attrs({ + "x": x, + "y": y, + "width": SQUARE_SIZE, + "height": SQUARE_SIZE, + "class": " ".join(cls), + "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 + margin + y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + margin + + 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 + margin + y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + margin + + 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", { + "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", _attrs({ + "href": "#xx", + "xlink:href": "#xx", + "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 = "green" + + try: + color, opacity = _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 orientation else 7.5 - tail_file) * SQUARE_SIZE + ytail = margin + (7.5 - tail_rank if orientation else tail_rank + 0.5) * SQUARE_SIZE + xhead = margin + (head_file + 0.5 if orientation else 7.5 - head_file) * SQUARE_SIZE + yhead = margin + (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", _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", + "class": "circle", + })) + else: + marker_size = 0.75 * SQUARE_SIZE + marker_margin = 0.1 * SQUARE_SIZE + + dx, dy = xhead - xtail, yhead - ytail + hypot = math.hypot(dx, dy) + + shaft_x = xhead - dx * (marker_size + marker_margin) / hypot + shaft_y = yhead - dy * (marker_size + marker_margin) / hypot + + xtip = xhead - dx * marker_margin / hypot + ytip = yhead - dy * marker_margin / hypot + + ET.SubElement(svg, "line", _attrs({ + "x1": xtail, + "y1": ytail, + "x2": shaft_x, + "y2": shaft_y, + "stroke": color, + "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, + shaft_y - dx * 0.5 * marker_size / hypot), + (shaft_x - dy * 0.5 * marker_size / hypot, + shaft_y + dx * 0.5 * marker_size / hypot)] + + ET.SubElement(svg, "polygon", _attrs({ + "points": " ".join(f"{x},{y}" for x, y in marker), + "fill": color, + "opacity": opacity if opacity < 1.0 else None, + "class": "arrow", + })) + + return SvgWrapper(ET.tostring(svg).decode("utf-8")) diff --git a/cartesi-python-chess-cartesi-img/chess/svg.pyc b/cartesi-python-chess-cartesi-img/chess/svg.pyc new file mode 100644 index 0000000..147127a Binary files /dev/null and b/cartesi-python-chess-cartesi-img/chess/svg.pyc differ diff --git a/cartesi-python-chess-cartesi-img/chess/syzygy.py b/cartesi-python-chess-cartesi-img/chess/syzygy.py new file mode 100644 index 0000000..b818f1a --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/syzygy.py @@ -0,0 +1,1996 @@ +# This file is part of the python-chess library. +# Copyright (C) 2012-2021 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 +import mmap +import os +import re +import struct +import threading +import typing + +import chess + +from types import TracebackType +from typing import Deque, Dict, Iterable, Iterator, List, Optional, Tuple, Type, TypeVar, Union + + +UINT64_BE = struct.Struct(">Q") +UINT32 = struct.Struct("I") +UINT16 = struct.Struct(" int: + return chess.square_rank(square) - chess.square_file(square) + +def flipdiag(square: chess.Square) -> chess.Square: + return ((square >> 3) | (square << 3)) & 63 + +LOWER = [ + 28, 0, 1, 2, 3, 4, 5, 6, + 0, 29, 7, 8, 9, 10, 11, 12, + 1, 7, 30, 13, 14, 15, 16, 17, + 2, 8, 13, 31, 18, 19, 20, 21, + 3, 9, 14, 18, 32, 22, 23, 24, + 4, 10, 15, 19, 22, 33, 25, 26, + 5, 11, 16, 20, 23, 25, 34, 27, + 6, 12, 17, 21, 24, 26, 27, 35, +] + +DIAG = [ + 0, 0, 0, 0, 0, 0, 0, 8, + 0, 1, 0, 0, 0, 0, 9, 0, + 0, 0, 2, 0, 0, 10, 0, 0, + 0, 0, 0, 3, 11, 0, 0, 0, + 0, 0, 0, 12, 4, 0, 0, 0, + 0, 0, 13, 0, 0, 5, 0, 0, + 0, 14, 0, 0, 0, 0, 6, 0, + 15, 0, 0, 0, 0, 0, 0, 7, +] + +FLAP = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 6, 12, 18, 18, 12, 6, 0, + 1, 7, 13, 19, 19, 13, 7, 1, + 2, 8, 14, 20, 20, 14, 8, 2, + 3, 9, 15, 21, 21, 15, 9, 3, + 4, 10, 16, 22, 22, 16, 10, 4, + 5, 11, 17, 23, 23, 17, 11, 5, + 0, 0, 0, 0, 0, 0, 0, 0, +] + +PTWIST = [ + 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, + 41, 29, 17, 5, 4, 16, 28, 40, + 39, 27, 15, 3, 2, 14, 26, 38, + 37, 25, 13, 1, 0, 12, 24, 36, + 0, 0, 0, 0, 0, 0, 0, 0, +] + +INVFLAP = [ + 8, 16, 24, 32, 40, 48, + 9, 17, 25, 33, 41, 49, + 10, 18, 26, 34, 42, 50, + 11, 19, 27, 35, 43, 51, +] + +FILE_TO_FILE = [0, 1, 2, 3, 3, 2, 1, 0] + +KK_IDX = [[ + -1, -1, -1, 0, 1, 2, 3, 4, + -1, -1, -1, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, + 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, +], [ + 58, -1, -1, -1, 59, 60, 61, 62, + 63, -1, -1, -1, 64, 65, 66, 67, + 68, 69, 70, 71, 72, 73, 74, 75, + 76, 77, 78, 79, 80, 81, 82, 83, + 84, 85, 86, 87, 88, 89, 90, 91, + 92, 93, 94, 95, 96, 97, 98, 99, + 100, 101, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 111, 112, 113, 114, 115, +], [ + 116, 117, -1, -1, -1, 118, 119, 120, + 121, 122, -1, -1, -1, 123, 124, 125, + 126, 127, 128, 129, 130, 131, 132, 133, + 134, 135, 136, 137, 138, 139, 140, 141, + 142, 143, 144, 145, 146, 147, 148, 149, + 150, 151, 152, 153, 154, 155, 156, 157, + 158, 159, 160, 161, 162, 163, 164, 165, + 166, 167, 168, 169, 170, 171, 172, 173, +], [ + 174, -1, -1, -1, 175, 176, 177, 178, + 179, -1, -1, -1, 180, 181, 182, 183, + 184, -1, -1, -1, 185, 186, 187, 188, + 189, 190, 191, 192, 193, 194, 195, 196, + 197, 198, 199, 200, 201, 202, 203, 204, + 205, 206, 207, 208, 209, 210, 211, 212, + 213, 214, 215, 216, 217, 218, 219, 220, + 221, 222, 223, 224, 225, 226, 227, 228, +], [ + 229, 230, -1, -1, -1, 231, 232, 233, + 234, 235, -1, -1, -1, 236, 237, 238, + 239, 240, -1, -1, -1, 241, 242, 243, + 244, 245, 246, 247, 248, 249, 250, 251, + 252, 253, 254, 255, 256, 257, 258, 259, + 260, 261, 262, 263, 264, 265, 266, 267, + 268, 269, 270, 271, 272, 273, 274, 275, + 276, 277, 278, 279, 280, 281, 282, 283, +], [ + 284, 285, 286, 287, 288, 289, 290, 291, + 292, 293, -1, -1, -1, 294, 295, 296, + 297, 298, -1, -1, -1, 299, 300, 301, + 302, 303, -1, -1, -1, 304, 305, 306, + 307, 308, 309, 310, 311, 312, 313, 314, + 315, 316, 317, 318, 319, 320, 321, 322, + 323, 324, 325, 326, 327, 328, 329, 330, + 331, 332, 333, 334, 335, 336, 337, 338, +], [ + -1, -1, 339, 340, 341, 342, 343, 344, + -1, -1, 345, 346, 347, 348, 349, 350, + -1, -1, 441, 351, 352, 353, 354, 355, + -1, -1, -1, 442, 356, 357, 358, 359, + -1, -1, -1, -1, 443, 360, 361, 362, + -1, -1, -1, -1, -1, 444, 363, 364, + -1, -1, -1, -1, -1, -1, 445, 365, + -1, -1, -1, -1, -1, -1, -1, 446, +], [ + -1, -1, -1, 366, 367, 368, 369, 370, + -1, -1, -1, 371, 372, 373, 374, 375, + -1, -1, -1, 376, 377, 378, 379, 380, + -1, -1, -1, 447, 381, 382, 383, 384, + -1, -1, -1, -1, 448, 385, 386, 387, + -1, -1, -1, -1, -1, 449, 388, 389, + -1, -1, -1, -1, -1, -1, 450, 390, + -1, -1, -1, -1, -1, -1, -1, 451, +], [ + 452, 391, 392, 393, 394, 395, 396, 397, + -1, -1, -1, -1, 398, 399, 400, 401, + -1, -1, -1, -1, 402, 403, 404, 405, + -1, -1, -1, -1, 406, 407, 408, 409, + -1, -1, -1, -1, 453, 410, 411, 412, + -1, -1, -1, -1, -1, 454, 413, 414, + -1, -1, -1, -1, -1, -1, 455, 415, + -1, -1, -1, -1, -1, -1, -1, 456, +], [ + 457, 416, 417, 418, 419, 420, 421, 422, + -1, 458, 423, 424, 425, 426, 427, 428, + -1, -1, -1, -1, -1, 429, 430, 431, + -1, -1, -1, -1, -1, 432, 433, 434, + -1, -1, -1, -1, -1, 435, 436, 437, + -1, -1, -1, -1, -1, 459, 438, 439, + -1, -1, -1, -1, -1, -1, 460, 440, + -1, -1, -1, -1, -1, -1, -1, 461, +]] + +PP_IDX = [[ + 0, -1, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, + -1, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, +], [ + 62, -1, -1, 63, 64, 65, -1, 66, + -1, 67, 68, 69, 70, 71, 72, -1, + 73, 74, 75, 76, 77, 78, 79, 80, + 81, 82, 83, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 93, 94, 95, 96, + -1, 97, 98, 99, 100, 101, 102, 103, + -1, 104, 105, 106, 107, 108, 109, -1, + 110, -1, 111, 112, 113, 114, -1, 115, +], [ + 116, -1, -1, -1, 117, -1, -1, 118, + -1, 119, 120, 121, 122, 123, 124, -1, + -1, 125, 126, 127, 128, 129, 130, -1, + 131, 132, 133, 134, 135, 136, 137, 138, + -1, 139, 140, 141, 142, 143, 144, 145, + -1, 146, 147, 148, 149, 150, 151, -1, + -1, 152, 153, 154, 155, 156, 157, -1, + 158, -1, -1, 159, 160, -1, -1, 161, +], [ + 162, -1, -1, -1, -1, -1, -1, 163, + -1, 164, -1, 165, 166, 167, 168, -1, + -1, 169, 170, 171, 172, 173, 174, -1, + -1, 175, 176, 177, 178, 179, 180, -1, + -1, 181, 182, 183, 184, 185, 186, -1, + -1, -1, 187, 188, 189, 190, 191, -1, + -1, 192, 193, 194, 195, 196, 197, -1, + 198, -1, -1, -1, -1, -1, -1, 199, +], [ + 200, -1, -1, -1, -1, -1, -1, 201, + -1, 202, -1, -1, 203, -1, 204, -1, + -1, -1, 205, 206, 207, 208, -1, -1, + -1, 209, 210, 211, 212, 213, 214, -1, + -1, -1, 215, 216, 217, 218, 219, -1, + -1, -1, 220, 221, 222, 223, -1, -1, + -1, 224, -1, 225, 226, -1, 227, -1, + 228, -1, -1, -1, -1, -1, -1, 229, +], [ + 230, -1, -1, -1, -1, -1, -1, 231, + -1, 232, -1, -1, -1, -1, 233, -1, + -1, -1, 234, -1, 235, 236, -1, -1, + -1, -1, 237, 238, 239, 240, -1, -1, + -1, -1, -1, 241, 242, 243, -1, -1, + -1, -1, 244, 245, 246, 247, -1, -1, + -1, 248, -1, -1, -1, -1, 249, -1, + 250, -1, -1, -1, -1, -1, -1, 251, +], [ + -1, -1, -1, -1, -1, -1, -1, 259, + -1, 252, -1, -1, -1, -1, 260, -1, + -1, -1, 253, -1, -1, 261, -1, -1, + -1, -1, -1, 254, 262, -1, -1, -1, + -1, -1, -1, -1, 255, -1, -1, -1, + -1, -1, -1, -1, -1, 256, -1, -1, + -1, -1, -1, -1, -1, -1, 257, -1, + -1, -1, -1, -1, -1, -1, -1, 258, +], [ + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, 268, -1, + -1, -1, 263, -1, -1, 269, -1, -1, + -1, -1, -1, 264, 270, -1, -1, -1, + -1, -1, -1, -1, 265, -1, -1, -1, + -1, -1, -1, -1, -1, 266, -1, -1, + -1, -1, -1, -1, -1, -1, 267, -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, 274, -1, -1, + -1, -1, -1, 271, 275, -1, -1, -1, + -1, -1, -1, -1, 272, -1, -1, -1, + -1, -1, -1, -1, -1, 273, -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, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, 277, -1, -1, -1, + -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, +]] + +def test45(sq: chess.Square) -> bool: + return bool(chess.BB_SQUARES[sq] & (chess.BB_A5 | chess.BB_A6 | chess.BB_A7 | + chess.BB_B5 | chess.BB_B6 | + chess.BB_C5)) + +MTWIST = [ + 15, 63, 55, 47, 40, 48, 56, 12, + 62, 11, 39, 31, 24, 32, 8, 57, + 54, 38, 7, 23, 16, 4, 33, 49, + 46, 30, 22, 3, 0, 17, 25, 41, + 45, 29, 21, 2, 1, 18, 26, 42, + 53, 37, 6, 20, 19, 5, 34, 50, + 61, 10, 36, 28, 27, 35, 9, 58, + 14, 60, 52, 44, 43, 51, 59, 13, +] + +def binom(x: int, y: int) -> int: + try: + return math.factorial(x) // math.factorial(y) // math.factorial(x - y) + except ValueError: + return 0 + +PAWNIDX = [[0 for _ in range(24)] for _ in range(5)] + +PFACTOR = [[0 for _ in range(4)] for _ in range(5)] + +for i in range(5): + j = 0 + + s = 0 + while j < 6: + PAWNIDX[i][j] = s + s += 1 if i == 0 else binom(PTWIST[INVFLAP[j]], i) + j += 1 + PFACTOR[i][0] = s + + s = 0 + while j < 12: + PAWNIDX[i][j] = s + s += 1 if i == 0 else binom(PTWIST[INVFLAP[j]], i) + j += 1 + PFACTOR[i][1] = s + + s = 0 + while j < 18: + PAWNIDX[i][j] = s + s += 1 if i == 0 else binom(PTWIST[INVFLAP[j]], i) + j += 1 + PFACTOR[i][2] = s + + s = 0 + while j < 24: + PAWNIDX[i][j] = s + s += 1 if i == 0 else binom(PTWIST[INVFLAP[j]], i) + j += 1 + PFACTOR[i][3] = s + +MULTIDX = [[0 for _ in range(10)] for _ in range(5)] + +MFACTOR = [0 for _ in range(5)] + +for i in range(5): + s = 0 + for j in range(10): + MULTIDX[i][j] = s + s += 1 if i == 0 else binom(MTWIST[INVTRIANGLE[j]], i) + MFACTOR[i] = s + +WDL_TO_MAP = [1, 3, 0, 2, 0] + +PA_FLAGS = [8, 0, 0, 0, 4] + +WDL_TO_DTZ = [-1, -101, 0, 101, 1] + +PCHR = ["K", "Q", "R", "B", "N", "P"] + +TABLENAME_REGEX = re.compile(r"^[KQRBNP]+v[KQRBNP]+\Z") + + +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 = [] + + white_pieces = piece_count - 2 + black_pieces = 0 + while white_pieces >= black_pieces: + targets.append(first + "P" * white_pieces + "v" + first + "P" * black_pieces) + white_pieces -= 1 + black_pieces += 1 + + return all_dependencies(targets, one_king=one_king) + + +def normalize_tablename(name: str, *, mirror: bool = False) -> str: + w, b = name.split("v", 1) + w = "".join(sorted(w, key=PCHR.index)) + b = "".join(sorted(b, key=PCHR.index)) + if mirror ^ ((len(w), [PCHR.index(c) for c in b]) < (len(b), [PCHR.index(c) for c in w])): + return b + "v" + w + else: + return w + "v" + b + + +def _dependencies(target: str, *, one_king: bool = True) -> Iterator[str]: + w, b = target.split("v", 1) + + for p in PCHR: + if p == "K" and one_king: + continue + + # Promotions. + if p != "P" and "P" in w: + yield normalize_tablename(w.replace("P", p, 1) + "v" + b) + if p != "P" and "P" in b: + yield normalize_tablename(w + "v" + b.replace("P", p, 1)) + + # Captures. + if p in w and len(w) > 1: + yield normalize_tablename(w.replace(p, "", 1) + "v" + b) + if p in b and len(b) > 1: + yield normalize_tablename(w + "v" + b.replace(p, "", 1)) + + +def dependencies(target: str, *, one_king: bool = True) -> Iterator[str]: + closed = set() + if one_king: + closed.add("KvK") + + for dependency in _dependencies(target, one_king=one_king): + if dependency not in closed and len(dependency) > 2: + yield dependency + closed.add(dependency) + + +def all_dependencies(targets: Iterable[str], *, one_king: bool = True) -> Iterator[str]: + closed = set() + if one_king: + closed.add("KvK") + + open_list = [normalize_tablename(target) for target in targets] + + while open_list: + name = open_list.pop() + if name in closed: + continue + + yield name + closed.add(name) + + open_list.extend(_dependencies(name, one_king=one_king)) + + +def calc_key(board: chess.BaseBoard, *, mirror: bool = False) -> str: + w = board.occupied_co[chess.WHITE ^ mirror] + b = board.occupied_co[chess.BLACK ^ mirror] + + return "".join([ + "K" * chess.popcount(board.kings & w), + "Q" * chess.popcount(board.queens & w), + "R" * chess.popcount(board.rooks & w), + "B" * chess.popcount(board.bishops & w), + "N" * chess.popcount(board.knights & w), + "P" * chess.popcount(board.pawns & w), + "v", + "K" * chess.popcount(board.kings & b), + "Q" * chess.popcount(board.queens & b), + "R" * chess.popcount(board.rooks & b), + "B" * chess.popcount(board.bishops & b), + "N" * chess.popcount(board.knights & b), + "P" * chess.popcount(board.pawns & b), + ]) + + +def recalc_key(pieces: List[chess.PieceType], *, mirror: bool = False) -> str: + # Some endgames are stored with a different key than their filename + # indicates: http://talkchess.com/forum/viewtopic.php?p=695509#695509 + + w = 8 if mirror else 0 + b = 0 if mirror else 8 + + return "".join([ + "K" * pieces.count(6 ^ w), + "Q" * pieces.count(5 ^ w), + "R" * pieces.count(4 ^ w), + "B" * pieces.count(3 ^ w), + "N" * pieces.count(2 ^ w), + "P" * pieces.count(1 ^ w), + "v", + "K" * pieces.count(6 ^ b), + "Q" * pieces.count(5 ^ b), + "R" * pieces.count(4 ^ b), + "B" * pieces.count(3 ^ b), + "N" * pieces.count(2 ^ b), + "P" * pieces.count(1 ^ b), + ]) + + +def subfactor(k: int, n: int) -> int: + f = n + l = 1 + + for i in range(1, k): + f *= n - i + l *= i + 1 + + return f // l + + +def dtz_before_zeroing(wdl: int) -> int: + return ((wdl > 0) - (wdl < 0)) * (1 if abs(wdl) == 2 else 101) + + +class MissingTableError(KeyError): + """Can not probe position because a required table is missing.""" + pass + + +class PairsData: + 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: Dict[int, PairsData] = {} + self.factor: Dict[int, List[int]] = {} + self.pieces: Dict[int, List[int]] = {} + self.norm: Dict[int, List[int]] = {} + + +class PawnFileDataDtz: + precomp: PairsData + factor: List[int] + pieces: List[int] + norm: List[int] + + +TableT = TypeVar("TableT", bound="Table") + +class Table: + size: List[int] + + def __init__(self, path: str, *, variant: Type[chess.Board] = chess.Board) -> None: + self.path = path + self.variant = variant + + self.write_lock = threading.RLock() + self.initialized = False + self.fd: Optional[int] = None + self.data: Optional[mmap.mmap] = None + + self.read_condition = threading.Condition() + self.read_count = 0 + + tablename, _ = os.path.splitext(os.path.basename(path)) + self.key = normalize_tablename(tablename) + self.mirrored_key = normalize_tablename(tablename, mirror=True) + self.symmetric = self.key == self.mirrored_key + + # Leave the v out of the tablename to get the number of pieces. + self.num = len(tablename) - 1 + + self.has_pawns = "P" in tablename + + black_part, white_part = tablename.split("v") + if self.has_pawns: + self.pawns = {0: white_part.count("P"), + 1: 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: + j = 0 + for piece_type in PCHR: + if black_part.count(piece_type) == 1: + j += 1 + if white_part.count(piece_type) == 1: + j += 1 + if j >= 3: + self.enc_type = 0 + elif j == 2: + self.enc_type = 2 + else: # only for suicide + j = 16 + for _ in range(16): + for piece_type in PCHR: + if 1 < black_part.count(piece_type) < j: + j = black_part.count(piece_type) + if 1 < white_part.count(piece_type) < j: + j = white_part.count(piece_type) + 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) + + # 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 + + try: + # Python 3.8 + self.data.madvise(mmap.MADV_RANDOM) + except AttributeError: + pass + + def check_magic(self, magic: Optional[bytes], pawnless_magic: Optional[bytes]) -> None: + assert self.data + + 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] + if self.data[data_ptr] & 0x80: + d.idxbits = 0 + if wdl: + d.min_len = self.data[data_ptr + 1] + else: + # http://www.talkchess.com/forum/viewtopic.php?p=698093#698093 + d.min_len = 1 if self.variant.captures_compulsory else 0 + self._next = data_ptr + 2 + self.size[size_idx + 0] = 0 + self.size[size_idx + 1] = 0 + self.size[size_idx + 2] = 0 + return d + + d.blocksize = self.data[data_ptr + 1] + d.idxbits = self.data[data_ptr + 2] + + real_num_blocks = self.read_uint32(data_ptr + 4) + num_blocks = real_num_blocks + self.data[data_ptr + 3] + max_len = self.data[data_ptr + 8] + min_len = self.data[data_ptr + 9] + h = max_len - min_len + 1 + num_syms = self.read_uint16(data_ptr + 10 + 2 * h) + + d.offset = data_ptr + 10 + d.symlen = [0 for _ in range(h * 8 + num_syms)] + d.sympat = data_ptr + 12 + 2 * h + d.min_len = min_len + + self._next = data_ptr + 12 + 2 * h + 3 * num_syms + (num_syms & 1) + + num_indices = (tb_size + (1 << d.idxbits) - 1) >> d.idxbits + self.size[size_idx + 0] = 6 * num_indices + self.size[size_idx + 1] = 2 * num_blocks + self.size[size_idx + 2] = (1 << d.blocksize) * real_num_blocks + + tmp = [0 for _ in range(num_syms)] + for i in range(num_syms): + if not tmp[i]: + self.calc_symlen(d, i, tmp) + + d.base = [0 for _ in range(h)] + d.base[h - 1] = 0 + for i in range(h - 2, -1, -1): + d.base[i] = (d.base[i + 1] + self.read_uint16(d.offset + i * 2) - self.read_uint16(d.offset + i * 2 + 2)) // 2 + for i in range(h): + d.base[i] <<= 64 - (min_len + i) + + d.offset -= 2 * d.min_len + + return d + + 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: + norm[0] = 2 + else: + norm[0] = self.enc_type - 1 + + i = norm[0] + while i < self.num: + j = i + while j < self.num and pieces[j] == pieces[i]: + norm[i] += 1 + j += 1 + 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] + + n = 64 - norm[0] + + f = 1 + i = norm[0] + k = 0 + while i < self.num or k == order: + if k == order: + factor[0] = f + if self.enc_type < 4: + f *= PIVFAC[self.enc_type] + else: + f *= MFACTOR[self.enc_type - 2] + else: + factor[i] = f + f *= subfactor(norm[i], n) + n -= norm[i] + i += norm[i] + k += 1 + + return f + + 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] + n = 64 - i + + fac = 1 + k = 0 + while i < self.num or k in [order, order2]: + if k == order: + factor[0] = fac + fac *= PFACTOR[norm[0] - 1][f] + elif k == order2: + factor[norm[0]] = fac + fac *= subfactor(norm[norm[0]], 48 - norm[0]) + else: + factor[i] = fac + fac *= subfactor(norm[i], n) + n -= norm[i] + i += norm[i] + k += 1 + + return fac + + 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] + + i = self.pawns[0] + self.pawns[1] + while i < self.num: + j = i + while j < self.num and pieces[j] == pieces[i]: + norm[i] += 1 + j += 1 + 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: + d.symlen[s] = 0 + else: + s1 = ((self.data[w + 1] & 0xf) << 8) | self.data[w] + if not tmp[s1]: + self.calc_symlen(d, s1, tmp) + if not tmp[s2]: + self.calc_symlen(d, s2, tmp) + d.symlen[s] = d.symlen[s1] + d.symlen[s2] + 1 + tmp[s] = 1 + + def pawn_file(self, pos: List[chess.Square]) -> int: + 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: List[int]) -> int: + n = self.num + + if self.enc_type < 3: + if pos[0] & 0x04: + for i in range(n): + pos[i] ^= 0x07 + + if pos[0] & 0x20: + for i in range(n): + pos[i] ^= 0x38 + + for i in range(n): + if offdiag(pos[i]): + break + if i < (3 if self.enc_type == 0 else 2) and offdiag(pos[i]) > 0: + for i in range(n): + pos[i] = flipdiag(pos[i]) + + if self.enc_type == 0: # 111 + i = int(pos[1] > pos[0]) + j = int(pos[2] > pos[0]) + int(pos[2] > pos[1]) + + if offdiag(pos[0]): + idx = TRIANGLE[pos[0]] * 63 * 62 + (pos[1] - i) * 62 + (pos[2] - j) + elif offdiag(pos[1]): + idx = 6 * 63 * 62 + DIAG[pos[0]] * 28 * 62 + LOWER[pos[1]] * 62 + pos[2] - j + elif offdiag(pos[2]): + idx = 6 * 63 * 62 + 4 * 28 * 62 + (DIAG[pos[0]]) * 7 * 28 + (DIAG[pos[1]] - i) * 28 + LOWER[pos[2]] + else: + idx = 6 * 63 * 62 + 4 * 28 * 62 + 4 * 7 * 28 + (DIAG[pos[0]] * 7 * 6) + (DIAG[pos[1]] - i) * 6 + (DIAG[pos[2]] - j) + i = 3 + elif self.enc_type == 2: # K2 + if not self.variant.connected_kings: + idx = KK_IDX[TRIANGLE[pos[0]]][pos[1]] + else: + i = pos[1] > pos[0] + + if offdiag(pos[0]): + idx = TRIANGLE[pos[0]] * 63 + (pos[1] - i) + elif offdiag(pos[1]): + idx = 6 * 63 + DIAG[pos[0]] * 28 + LOWER[pos[1]] + else: + idx = 6 * 63 + 4 * 28 + (DIAG[pos[0]]) * 7 + (DIAG[pos[1]] - i) + + i = 2 + elif self.enc_type == 3: # 2, e.g. KKvK + if TRIANGLE[pos[0]] > TRIANGLE[pos[1]]: + pos[0], pos[1] = pos[1], pos[0] + if pos[0] & 0x04: + for i in range(n): + pos[i] ^= 0x07 + if pos[0] & 0x20: + for i in range(n): + pos[i] ^= 0x38 + if offdiag(pos[0]) > 0 or (offdiag(pos[0]) == 0 and offdiag(pos[1]) > 0): + for i in range(n): + pos[i] = flipdiag(pos[i]) + if test45(pos[1]) and TRIANGLE[pos[0]] == TRIANGLE[pos[1]]: + pos[0], pos[1] = pos[1], pos[0] + for i in range(n): + pos[i] = flipdiag(pos[i] ^ 0x38) + idx = PP_IDX[TRIANGLE[pos[0]]][pos[1]] + i = 2 + else: # 3 and higher, e.g. KKKvK and KKKKvK + for i in range(1, norm[0]): + if TRIANGLE[pos[0]] > TRIANGLE[pos[i]]: + pos[0], pos[i] = pos[i], pos[0] + if pos[0] & 0x04: + for i in range(n): + pos[i] ^= 0x07 + if pos[0] & 0x20: + for i in range(n): + pos[i] ^= 0x38 + if offdiag(pos[0]) > 0: + for i in range(n): + pos[i] = flipdiag(pos[i]) + for i in range(1, norm[0]): + for j in range(i + 1, norm[0]): + if MTWIST[pos[i]] > MTWIST[pos[j]]: + pos[i], pos[j] = pos[j], pos[i] + + idx = MULTIDX[norm[0] - 1][TRIANGLE[pos[0]]] + i = 1 + while i < norm[0]: + idx += binom(MTWIST[pos[i]], i) + i += 1 + + idx *= factor[0] + + while i < n: + t = norm[i] + + for j in range(i, i + t): + for k in range(j + 1, i + t): + # Swap. + if pos[j] > pos[k]: + pos[j], pos[k] = pos[k], pos[j] + + s = 0 + + for m in range(i, i + t): + p = pos[m] + j = 0 + for l in range(i): + j += int(p > pos[l]) + s += binom(p - j, m - i + 1) + + idx += s * factor[i] + i += t + + return idx + + def encode_pawn(self, norm: List[int], pos: List[chess.Square], factor: List[int]) -> int: + n = self.num + + if pos[0] & 0x04: + for i in range(n): + pos[i] ^= 0x07 + + for i in range(1, self.pawns[0]): + for j in range(i + 1, self.pawns[0]): + if PTWIST[pos[i]] < PTWIST[pos[j]]: + pos[i], pos[j] = pos[j], pos[i] + + t = self.pawns[0] - 1 + idx = PAWNIDX[t][FLAP[pos[0]]] + for i in range(t, 0, -1): + idx += binom(PTWIST[pos[i]], t - i + 1) + idx *= factor[0] + + # Remaining pawns. + i = self.pawns[0] + t = i + self.pawns[1] + if t > i: + for j in range(i, t): + for k in range(j + 1, t): + if pos[j] > pos[k]: + pos[j], pos[k] = pos[k], pos[j] + s = 0 + for m in range(i, t): + p = pos[m] + j = 0 + for k in range(i): + j += int(p > pos[k]) + s += binom(p - j - 8, m - i + 1) + idx += s * factor[i] + i = t + + while i < n: + t = norm[i] + for j in range(i, i + t): + for k in range(j + 1, i + t): + if pos[j] > pos[k]: + pos[j], pos[k] = pos[k], pos[j] + + s = 0 + for m in range(i, i + t): + p = pos[m] + j = 0 + for k in range(i): + j += int(p > pos[k]) + s += binom(p - j, m - i + 1) + + idx += s * factor[i] + i += t + + return idx + + def decompress_pairs(self, d: PairsData, idx: int) -> int: + assert self.data + + if not d.idxbits: + return d.min_len + + mainidx = idx >> d.idxbits + litidx = (idx & (1 << d.idxbits) - 1) - (1 << (d.idxbits - 1)) + block = self.read_uint32(d.indextable + 6 * mainidx) + + idx_offset = self.read_uint16(d.indextable + 6 * mainidx + 4) + litidx += idx_offset + + if litidx < 0: + while litidx < 0: + block -= 1 + litidx += self.read_uint16(d.sizetable + 2 * block) + 1 + else: + while litidx > self.read_uint16(d.sizetable + 2 * block): + litidx -= self.read_uint16(d.sizetable + 2 * block) + 1 + block += 1 + + ptr = d.data + (block << d.blocksize) + + m = d.min_len + base_idx = -m + symlen_idx = 0 + + code = self.read_uint64_be(ptr) + + ptr += 2 * 4 + bitcnt = 0 # Number of empty bits in code + while True: + l = m + while code < d.base[base_idx + l]: + l += 1 + sym = self.read_uint16(d.offset + l * 2) + sym += (code - d.base[base_idx + l]) >> (64 - l) + if litidx < d.symlen[symlen_idx + sym] + 1: + break + litidx -= d.symlen[symlen_idx + sym] + 1 + code <<= l + bitcnt += l + if bitcnt >= 32: + bitcnt -= 32 + code |= self.read_uint32_be(ptr) << bitcnt + ptr += 4 + + # Cut off at 64bit. + code &= 0xffffffffffffffff + + sympat = d.sympat + while d.symlen[symlen_idx + sym]: + w = sympat + 3 * sym + s1 = ((self.data[w + 1] & 0xf) << 8) | self.data[w] + if litidx < d.symlen[symlen_idx + s1] + 1: + sym = s1 + else: + litidx -= d.symlen[symlen_idx + s1] + 1 + sym = (self.data[w + 2] << 4) | (self.data[w + 1] >> 4) + + w = sympat + 3 * sym + if isinstance(self, DtzTable): + return ((self.data[w + 1] & 0x0f) << 8) | self.data[w] + else: + return self.data[w] + + def read_uint64_be(self, data_ptr: int) -> int: + 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] # type: ignore + + def read_uint32_be(self, data_ptr: int) -> int: + 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] # type: ignore + + def close(self) -> None: + with self.write_lock: + with self.read_condition: + while self.read_count > 0: + self.read_condition.wait() + + if self.data is not None: + self.data.close() + self.data = None + + if self.fd is not None: + os.close(self.fd) + self.fd = None + + def __enter__(self: TableT) -> TableT: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.close() + + +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 + + self.check_magic(self.variant.tbw_magic, self.variant.pawnless_tbw_magic) + + self.tb_size = [0 for _ in range(8)] + self.size = [0 for _ in range(8 * 3)] + + # Used if there are only pieces. + self.precomp: Dict[int, PairsData] = {} + self.pieces: Dict[int, List[int]] = {} + + 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)]} + + # Used if there are pawns. + self.files = [PawnFileData() for _ in range(4)] + + split = self.data[4] & 0x01 + files = 4 if self.data[4] & 0x02 else 1 + + data_ptr = 5 + + if not self.has_pawns: + self.setup_pieces_piece(data_ptr) + data_ptr += self.num + 1 + data_ptr += data_ptr & 0x01 + + self.precomp[0] = self.setup_pairs(data_ptr, self.tb_size[0], 0, True) + data_ptr = self._next + if split: + self.precomp[1] = self.setup_pairs(data_ptr, self.tb_size[1], 3, True) + data_ptr = self._next + + self.precomp[0].indextable = data_ptr + data_ptr += self.size[0] + if split: + self.precomp[1].indextable = data_ptr + data_ptr += self.size[3] + + self.precomp[0].sizetable = data_ptr + data_ptr += self.size[1] + if split: + self.precomp[1].sizetable = data_ptr + data_ptr += self.size[4] + + data_ptr = (data_ptr + 0x3f) & ~0x3f + self.precomp[0].data = data_ptr + data_ptr += self.size[2] + if split: + data_ptr = (data_ptr + 0x3f) & ~0x3f + self.precomp[1].data = data_ptr + + self.key = recalc_key(self.pieces[0]) + self.mirrored_key = recalc_key(self.pieces[0], mirror=True) + else: + s = 1 + int(self.pawns[1] > 0) + for f in range(4): + self.setup_pieces_pawn(data_ptr, 2 * f, f) + data_ptr += self.num + s + data_ptr += data_ptr & 0x01 + + for f in range(files): + self.files[f].precomp[0] = self.setup_pairs(data_ptr, self.tb_size[2 * f], 6 * f, True) + data_ptr = self._next + 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 + + for f in range(files): + self.files[f].precomp[0].indextable = data_ptr + data_ptr += self.size[6 * f] + if split: + self.files[f].precomp[1].indextable = data_ptr + data_ptr += self.size[6 * f + 3] + + for f in range(files): + self.files[f].precomp[0].sizetable = data_ptr + data_ptr += self.size[6 * f + 1] + if split: + self.files[f].precomp[1].sizetable = data_ptr + data_ptr += self.size[6 * f + 4] + + for f in range(files): + data_ptr = (data_ptr + 0x3f) & ~0x3f + self.files[f].precomp[0].data = data_ptr + data_ptr += self.size[6 * f + 2] + if split: + data_ptr = (data_ptr + 0x3f) & ~0x3f + self.files[f].precomp[1].data = data_ptr + data_ptr += self.size[6 * f + 5] + + 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 + self.files[f].pieces[0] = [self.data[p_data + i + j] & 0x0f for i in range(self.num)] + self.files[f].norm[0] = [0 for _ in range(self.num)] + self.set_norm_pawn(self.files[f].norm[0], self.files[f].pieces[0]) + self.files[f].factor[0] = [0 for _ in range(TBPIECES)] + self.tb_size[p_tb_size] = self.calc_factors_pawn(self.files[f].factor[0], order, order2, self.files[f].norm[0], f) + + order = self.data[p_data] >> 4 + order2 = self.data[p_data + 1] >> 4 if self.pawns[1] else 0x0f + self.files[f].pieces[1] = [self.data[p_data + i + j] >> 4 for i in range(self.num)] + self.files[f].norm[1] = [0 for _ in range(self.num)] + self.set_norm_pawn(self.files[f].norm[1], self.files[f].pieces[1]) + self.files[f].factor[1] = [0 for _ in range(TBPIECES)] + 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]) + self.tb_size[0] = self.calc_factors_piece(self.factor[0], order, self.norm[0]) + + self.pieces[1] = [self.data[p_data + i + 1] >> 4 for i in range(self.num)] + order = self.data[p_data] >> 4 + self.set_norm_piece(self.norm[1], self.pieces[1]) + self.tb_size[1] = self.calc_factors_piece(self.factor[1], order, self.norm[1]) + + def probe_wdl_table(self, board: chess.Board) -> int: + try: + with self.read_condition: + self.read_count += 1 + return self._probe_wdl_table(board) + finally: + with self.read_condition: + self.read_count -= 1 + self.read_condition.notify() + + def _probe_wdl_table(self, board: chess.Board) -> int: + self.init_table_wdl() + + key = calc_key(board) + + if not self.symmetric: + if key != self.key: + cmirror = 8 + mirror = 0x38 + bside = int(board.turn == chess.WHITE) + else: + cmirror = mirror = 0 + bside = int(board.turn != chess.WHITE) + else: + cmirror = 0 if board.turn == chess.WHITE else 8 + mirror = 0 if board.turn == chess.WHITE else 0x38 + bside = 0 + + if not self.has_pawns: + p = [0 for _ in range(TBPIECES)] + i = 0 + while i < self.num: + piece_type = self.pieces[bside][i] & 0x07 + color = (self.pieces[bside][i] ^ cmirror) >> 3 + bb = board.pieces_mask(piece_type, chess.WHITE if color == 0 else chess.BLACK) + + for square in chess.scan_forward(bb): + p[i] = square + i += 1 + + idx = self.encode_piece(self.norm[bside], p, self.factor[bside]) + res = self.decompress_pairs(self.precomp[bside], idx) + else: + p = [0 for _ in range(TBPIECES)] + i = 0 + k = self.files[0].pieces[0][0] ^ cmirror + color = k >> 3 + piece_type = k & 0x07 + bb = board.pieces_mask(piece_type, chess.WHITE if color == 0 else chess.BLACK) + + for square in chess.scan_forward(bb): + p[i] = square ^ mirror + i += 1 + + f = self.pawn_file(p) + pc = self.files[f].pieces[bside] + while i < self.num: + color = (pc[i] ^ cmirror) >> 3 + piece_type = pc[i] & 0x07 + bb = board.pieces_mask(piece_type, chess.WHITE if color == 0 else chess.BLACK) + + for square in chess.scan_forward(bb): + p[i] = square ^ mirror + i += 1 + + idx = self.encode_pawn(self.files[f].norm[bside], p, self.files[f].factor[bside]) + res = self.decompress_pairs(self.files[f].precomp[bside], idx) + + return res - 2 + + +class DtzTable(Table): + + def init_table_dtz(self) -> None: + with self.write_lock: + self.init_mmap() + assert self.data + + if self.initialized: + return + + self.check_magic(self.variant.tbz_magic, self.variant.pawnless_tbz_magic) + + self.factor = [0 for _ in range(TBPIECES)] + 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)] + + 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.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: 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[0][i] = p_data + 1 - self.p_map + p_data += 1 + self.data[p_data] + else: + for i in range(4): + 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 + + self.precomp.indextable = p_data + p_data += self.size[0] + + self.precomp.sizetable = p_data + p_data += self.size[1] + + p_data = (p_data + 0x3f) & ~0x3f + self.precomp.data = p_data + p_data += self.size[2] + + self.key = recalc_key(self.pieces) + self.mirrored_key = recalc_key(self.pieces, mirror=True) + else: + s = 1 + int(self.pawns[1] > 0) + for f in range(4): + self.setup_pieces_pawn_dtz(p_data, f, f) + p_data += self.num + s + p_data += p_data & 0x01 + + self.flags = [] + for f in range(files): + self.files[f].precomp = self.setup_pairs(p_data, self.tb_size[f], 3 * f, False) + p_data = self._next + self.flags.append(self._flags) + + self.map_idx = [] + self.p_map = p_data + for f in range(files): + self.map_idx.append([]) + if self.flags[f] & 2: + if not self.flags[f] & 16: + for _ in range(4): + self.map_idx[-1].append(p_data + 1 - self.p_map) + p_data += 1 + self.data[p_data] + else: + p_data += p_data & 0x01 + for _ in range(4): + self.map_idx[-1].append((p_data + 2 - self.p_map) // 2) + p_data += 2 + 2 * self.read_uint16(p_data) + p_data += p_data & 0x01 + + for f in range(files): + self.files[f].precomp.indextable = p_data + p_data += self.size[3 * f] + + for f in range(files): + self.files[f].precomp.sizetable = p_data + p_data += self.size[3 * f + 1] + + for f in range(files): + p_data = (p_data + 0x3f) & ~0x3f + self.files[f].precomp.data = p_data + p_data += self.size[3 * f + 2] + + self.initialized = True + + def probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: + try: + with self.read_condition: + self.read_count += 1 + return self._probe_dtz_table(board, wdl) + finally: + with self.read_condition: + self.read_count -= 1 + self.read_condition.notify() + + def _probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: + self.init_table_dtz() + assert self.data + + key = calc_key(board) + + if not self.symmetric: + if key != self.key: + cmirror = 8 + mirror = 0x38 + bside = int(board.turn == chess.WHITE) + else: + cmirror = mirror = 0 + bside = int(board.turn != chess.WHITE) + else: + cmirror = 0 if board.turn == chess.WHITE else 8 + mirror = 0 if board.turn == chess.WHITE else 0x38 + bside = 0 + + if not self.has_pawns: + assert isinstance(self.flags, int) + + if (self.flags & 1) != bside and not self.symmetric: + return 0, -1 + + pc = self.pieces + p = [0 for _ in range(TBPIECES)] + i = 0 + while i < self.num: + piece_type = pc[i] & 0x07 + color = (pc[i] ^ cmirror) >> 3 + bb = board.pieces_mask(piece_type, chess.WHITE if color == 0 else chess.BLACK) + + for square in chess.scan_forward(bb): + p[i] = square + i += 1 + + idx = self.encode_piece(self.norm, p, self.factor) + res = self.decompress_pairs(self.precomp, idx) + + if self.flags & 2: + if not self.flags & 16: + 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[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 + bb = board.pieces_mask(piece_type, chess.WHITE if color == 0 else chess.BLACK) + + i = 0 + p = [0 for _ in range(TBPIECES)] + for square in chess.scan_forward(bb): + p[i] = square ^ mirror + i += 1 + f = self.pawn_file(p) + if self.flags[f] & 1 != bside: + return 0, -1 + + pc = self.files[f].pieces + while i < self.num: + piece_type = pc[i] & 0x07 + color = (pc[i] ^ cmirror) >> 3 + bb = board.pieces_mask(piece_type, chess.WHITE if color == 0 else chess.BLACK) + + for square in chess.scan_forward(bb): + p[i] = square ^ mirror + i += 1 + + idx = self.encode_pawn(self.files[f].norm, p, self.files[f].factor) + res = self.decompress_pairs(self.files[f].precomp, idx) + + if self.flags[f] & 2: + if not self.flags[f] & 16: + res = self.data[self.p_map + self.map_idx[f][WDL_TO_MAP[wdl + 2]] + res] + else: + res = self.read_uint16(self.p_map + 2 * (self.map_idx[f][WDL_TO_MAP[wdl + 2]] + res)) + + if (not (self.flags[f] & PA_FLAGS[wdl + 2])) or (wdl & 1): + res *= 2 + + 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 + self.files[f].pieces = [self.data[p_data + i + j] & 0x0f for i in range(self.num)] + + self.files[f].norm = [0 for _ in range(self.num)] + self.set_norm_pawn(self.files[f].norm, self.files[f].pieces) + + self.files[f].factor = [0 for _ in range(TBPIECES)] + self.tb_size[p_tb_size] = self.calc_factors_pawn(self.files[f].factor, order, order2, self.files[f].norm, f) + + +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 + + self.max_fds = max_fds + self.lru: Deque[Table] = collections.deque() + self.lru_lock = threading.Lock() + + self.wdl: Dict[str, Table] = {} + self.dtz: Dict[str, Table] = {} + + def _bump_lru(self, table: Table) -> None: + if self.max_fds is None: + return + + with self.lru_lock: + try: + self.lru.remove(table) + self.lru.appendleft(table) + except ValueError: + self.lru.appendleft(table) + + if len(self.lru) > self.max_fds: + self.lru.pop().close() + + 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: + hashtable[table.key].close() + + hashtable[table.key] = table + hashtable[table.mirrored_key] = table + return 1 + + def add_directory(self, directory: str, *, load_wdl: bool = True, load_dtz: bool = True) -> int: + """ + 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``) + are added. + + The relevant files are lazily opened when the tablebase is actually + probed. + + 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_tablename(tablename, one_king=self.variant.one_king) 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 + + def probe_wdl_table(self, board: chess.Board) -> int: + # Test for variant end. + if board.is_variant_win(): + return 2 + elif board.is_variant_draw(): + return 0 + elif board.is_variant_loss(): + return -2 + + # Test for KvK. + if self.variant.one_king and board.kings == board.occupied: + return 0 + + key = calc_key(board) + try: + table = typing.cast(WdlTable, self.wdl[key]) + except KeyError: + raise MissingTableError(f"did not find wdl table {key}") + + self._bump_lru(table) + + return table.probe_wdl_table(board) + + def probe_ab(self, board: chess.Board, alpha: int, beta: int, threats: bool = False) -> Tuple[int, int]: + if self.variant.captures_compulsory: + if board.is_variant_win(): + return 2, 2 + elif board.is_variant_loss(): + return -2, 2 + elif board.is_variant_draw(): + return 0, 2 + + return self.sprobe_ab(board, alpha, beta, threats) + + # Generate non-ep captures. + for move in board.generate_legal_moves(to_mask=board.occupied_co[not board.turn]): + board.push(move) + try: + v_plus, _ = self.probe_ab(board, -beta, -alpha) + v = -v_plus + finally: + board.pop() + + if v > alpha: + if v >= beta: + return v, 2 + alpha = v + + v = self.probe_wdl_table(board) + + if alpha >= v: + return alpha, 1 + int(alpha > 0) + else: + return v, 1 + + def sprobe_ab(self, board: chess.Board, alpha: int, beta: int, threats: bool = False) -> Tuple[int, int]: + if chess.popcount(board.occupied_co[not board.turn]) > 1: + v, captures_found = self.sprobe_capts(board, alpha, beta) + if captures_found: + return v, 2 + else: + if any(board.generate_legal_captures()): + return -2, 2 + + threats_found = False + + if threats or chess.popcount(board.occupied) >= 6: + for threat in board.generate_legal_moves(~board.pawns): + board.push(threat) + try: + v_plus, captures_found = self.sprobe_capts(board, -beta, -alpha) + v = -v_plus + finally: + board.pop() + + if captures_found and v > alpha: + threats_found = True + alpha = v + if alpha >= beta: + return v, 3 + + v = self.probe_wdl_table(board) + if v > alpha: + return v, 1 + else: + return alpha, 3 if threats_found else 1 + + def sprobe_capts(self, board: chess.Board, alpha: int, beta: int) -> Tuple[int, int]: + captures_found = False + + for move in board.generate_legal_captures(): + captures_found = True + + board.push(move) + try: + v_plus, _ = self.sprobe_ab(board, -beta, -alpha) + v = -v_plus + finally: + board.pop() + + alpha = max(v, alpha) + + if alpha >= beta: + break + + return alpha, captures_found + + def probe_wdl(self, board: chess.Board) -> int: + """ + 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. + + Returns ``2`` if the side to move is winning, ``0`` if the position is + a draw and ``-2`` if the side to move is losing. + + Returns ``1`` in case of a cursed win and ``-1`` in case of a blessed + loss. Mate can be forced but the position can be drawn due to the + fifty-move rule. + + >>> import chess + >>> import chess.syzygy + >>> + >>> with chess.syzygy.open_tablebase("data/syzygy/regular") as tablebase: + ... board = chess.Board("8/2K5/4B3/3N4/8/8/4k3/8 b - - 0 1") + ... print(tablebase.probe_wdl(board)) + ... + -2 + + :raises: :exc:`KeyError` (or specifically + :exc:`chess.syzygy.MissingTableError`) if the position could not + be found in the tablebase. Use + :func:`~chess.syzygy.Tablebase.get_wdl()` if you prefer to get + ``None`` instead of an exception. + + 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) > TBPIECES: + raise KeyError(f"syzygy tables support up to {TBPIECES} pieces, not {chess.popcount(board.occupied)}: {board.fen()}") + + # Probe. + v, _ = self.probe_ab(board, -2, 2) + + # If en passant is not possible, we are done. + if not board.ep_square or self.variant.captures_compulsory: + return v + + # Now handle en passant. + v1 = -3 + + # Look at all legal en passant captures. + for move in board.generate_legal_ep(): + board.push(move) + try: + v0_plus, _ = self.probe_ab(board, -2, 2) + v0 = -v0_plus + finally: + board.pop() + + if v0 > v1: + v1 = v0 + + if v1 > -3: + if v1 >= v: + v = v1 + elif v == 0: + # If there is not at least one legal non-en-passant move we are + # forced to play the losing en passant cature. + if all(board.is_en_passant(move) for move in board.generate_legal_moves()): + v = v1 + + return v + + def get_wdl(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: + try: + return self.probe_wdl(board) + except KeyError: + return default + + def probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: + key = calc_key(board) + try: + table = typing.cast(DtzTable, self.dtz[key]) + except KeyError: + raise MissingTableError(f"did not find dtz table {key}") + + self._bump_lru(table) + + return table.probe_dtz_table(board, wdl) + + def probe_dtz_no_ep(self, board: chess.Board) -> int: + wdl, success = self.probe_ab(board, -2, 2, threats=True) + + if wdl == 0: + return 0 + + if success == 2 or not board.occupied_co[board.turn] & ~board.pawns: + return dtz_before_zeroing(wdl) + + if wdl > 0: + # The position is a win or a cursed win by a threat move. + if success == 3: + return 2 if wdl == 2 else 102 + + # Generate all legal non-capturing pawn moves. + for move in board.generate_legal_moves(board.pawns, ~board.occupied): + if board.is_capture(move): + # En passant. + continue + + board.push(move) + try: + v = -self.probe_wdl(board) + finally: + board.pop() + + if v == wdl: + return 1 if v == 2 else 101 + + dtz, success = self.probe_dtz_table(board, wdl) + if success >= 0: + return dtz_before_zeroing(wdl) + (dtz if wdl > 0 else -dtz) + + if wdl > 0: + best = 0xffff + + for move in board.generate_legal_moves(~board.pawns, ~board.occupied): + board.push(move) + try: + v = -self.probe_dtz(board) + + if v == 1 and board.is_checkmate(): + best = 1 + elif v > 0 and v + 1 < best: + best = v + 1 + finally: + board.pop() + + return best + else: + best = -1 + + for move in board.generate_legal_moves(): + board.push(move) + + try: + if board.halfmove_clock == 0: + if wdl == -2: + v = -1 + else: + v, success = self.probe_ab(board, 1, 2, threats=True) + v = 0 if v == 2 else -101 + else: + v = -self.probe_dtz(board) - 1 + finally: + board.pop() + + if v < best: + best = v + + return best + + def probe_dtz(self, board: chess.Board) -> int: + """ + Probes DTZ tables for + `DTZ50'' information with rounding `_. + + 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. + More precisely: + + +-----+------------------+--------------------------------------------+ + | WDL | DTZ | | + +=====+==================+============================================+ + | -2 | -100 <= n <= -1 | Unconditional loss (assuming 50-move | + | | | counter is zero), where a zeroing move can | + | | | be forced in -n plies. | + +-----+------------------+--------------------------------------------+ + | -1 | n < -100 | Loss, but draw under the 50-move rule. | + | | | A zeroing move can be forced in -n plies | + | | | or -n - 100 plies (if a later phase is | + | | | responsible for the blessed loss). | + +-----+------------------+--------------------------------------------+ + | 0 | 0 | Draw. | + +-----+------------------+--------------------------------------------+ + | 1 | 100 < n | Win, but draw under the 50-move rule. | + | | | A zeroing move can be forced in n plies or | + | | | n - 100 plies (if a later phase is | + | | | responsible for the cursed win). | + +-----+------------------+--------------------------------------------+ + | 2 | 1 <= n <= 100 | Unconditional win (assuming 50-move | + | | | counter is zero), where a zeroing move can | + | | | be forced in n plies. | + +-----+------------------+--------------------------------------------+ + + 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 + winning zeroing move in n + 1 plies. + 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. + + 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 + >>> + >>> with chess.syzygy.open_tablebase("data/syzygy/regular") as tablebase: + ... board = chess.Board("8/2K5/4B3/3N4/8/8/4k3/8 b - - 0 1") + ... print(tablebase.probe_dtz(board)) + ... + -53 + + 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 + :func:`~chess.syzygy.Tablebase.get_dtz()` if you prefer to get + ``None`` instead of an exception. + + Note that probing corrupted table files is undefined behavior. + """ + v = self.probe_dtz_no_ep(board) + + if not board.ep_square or self.variant.captures_compulsory: + return v + + v1 = -3 + + # Generate all en passant moves. + for move in board.generate_legal_ep(): + board.push(move) + try: + v0_plus, _ = self.probe_ab(board, -2, 2) + v0 = -v0_plus + finally: + board.pop() + + if v0 > v1: + v1 = v0 + + if v1 > -3: + v1 = WDL_TO_DTZ[v1 + 2] + if v < -100: + if v1 >= 0: + v = v1 + elif v < 0: + if v1 >= 0 or v1 < -100: + v = v1 + elif v > 100: + if v1 > 0: + v = v1 + elif v > 0: + if v1 == 1: + v = v1 + elif v1 >= 0: + v = v1 + else: + if all(board.is_en_passant(move) for move in board.generate_legal_moves()): + v = v1 + + return v + + def get_dtz(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: + try: + return self.probe_dtz(board) + except KeyError: + return default + + def close(self) -> None: + """Closes all loaded tables.""" + while self.wdl: + _, wdl = self.wdl.popitem() + wdl.close() + + while self.dtz: + _, dtz = self.dtz.popitem() + dtz.close() + + self.lru.clear() + + def __enter__(self) -> Tablebase: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.close() + + +def open_tablebase(directory: str, *, load_wdl: bool = True, load_dtz: bool = True, max_fds: Optional[int] = 128, VariantBoard: Type[chess.Board] = chess.Board) -> Tablebase: + """ + Opens a collection of tables for probing. See + :class:`~chess.syzygy.Tablebase`. + + .. note:: + + Generally probing requires tablebase files for the specific + 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. + """ + tables = Tablebase(max_fds=max_fds, VariantBoard=VariantBoard) + tables.add_directory(directory, load_wdl=load_wdl, load_dtz=load_dtz) + return tables diff --git a/cartesi-python-chess-cartesi-img/chess/variant.py b/cartesi-python-chess-cartesi-img/chess/variant.py new file mode 100644 index 0000000..82f8416 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/chess/variant.py @@ -0,0 +1,1080 @@ +# This file is part of the python-chess library. +# Copyright (C) 2016-2021 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 + +from typing import Dict, Generic, Hashable, Iterable, Iterator, List, Optional, Type, TypeVar, Union + + +class SuicideBoard(chess.Board): + + aliases = ["Suicide", "Suicide chess"] + uci_variant = "suicide" + xboard_variant = "suicide" + + tbw_suffix = ".stbw" + tbz_suffix = ".stbz" + tbw_magic = b"\x7b\xf6\x93\x15" + tbz_magic = b"\xe4\xcf\xe7\x23" + pawnless_tbw_suffix = ".gtbw" + pawnless_tbz_suffix = ".gtbz" + pawnless_tbw_magic = b"\xbc\x55\xbc\x21" + pawnless_tbz_magic = b"\xd6\xf5\x1b\x50" + connected_kings = True + one_king = False + captures_compulsory = True + + def pin_mask(self, color: chess.Color, square: chess.Square) -> chess.Bitboard: + return chess.BB_ALL + + def _attacked_for_king(self, path: chess.Bitboard, occupied: chess.Bitboard) -> bool: + return False + + 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 + + def was_into_check(self) -> bool: + return False + + def _material_balance(self) -> int: + return (chess.popcount(self.occupied_co[self.turn]) - + chess.popcount(self.occupied_co[not self.turn])) + + def is_variant_end(self) -> bool: + return not all(has_pieces for has_pieces in self.occupied_co) + + def is_variant_win(self) -> bool: + if not self.occupied_co[self.turn]: + return True + else: + return self.is_stalemate() and self._material_balance() < 0 + + def is_variant_loss(self) -> bool: + if not self.occupied_co[self.turn]: + return False + else: + return self.is_stalemate() and self._material_balance() > 0 + + def is_variant_draw(self) -> bool: + if not self.occupied_co[self.turn]: + return False + else: + return self.is_stalemate() and self._material_balance() == 0 + + def has_insufficient_material(self, color: chess.Color) -> bool: + if self.occupied != self.bishops: + 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): + # Add king promotions. + if move.promotion == chess.QUEEN: + yield chess.Move(move.from_square, move.to_square, chess.KING) + + yield move + + def generate_legal_moves(self, from_mask: chess.Bitboard = chess.BB_ALL, to_mask: chess.Bitboard = chess.BB_ALL) -> Iterator[chess.Move]: + if self.is_variant_end(): + return + + # Generate captures first. + found_capture = False + for move in self.generate_pseudo_legal_captures(): + if chess.BB_SQUARES[move.from_square] & from_mask and chess.BB_SQUARES[move.to_square] & to_mask: + yield move + found_capture = True + + # Captures are mandatory. Stop here if any were found. + if not found_capture: + not_them = to_mask & ~self.occupied_co[not self.turn] + for move in self.generate_pseudo_legal_moves(from_mask, not_them): + if not self.is_en_passant(move): + yield move + + def is_legal(self, move: chess.Move) -> bool: + if not super().is_legal(move): + return False + + if self.is_capture(move): + return True + 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 status(self) -> chess.Status: + status = super().status() + status &= ~chess.STATUS_NO_WHITE_KING + status &= ~chess.STATUS_NO_BLACK_KING + status &= ~chess.STATUS_TOO_MANY_KINGS + status &= ~chess.STATUS_OPPOSITE_CHECK + return status + + +class GiveawayBoard(SuicideBoard): + + aliases = ["Giveaway", "Giveaway chess", "Give away", "Give away chess"] + uci_variant = "giveaway" + xboard_variant = "giveaway" + + tbw_suffix = ".gtbw" + tbz_suffix = ".gtbz" + tbw_magic = b"\xbc\x55\xbc\x21" + tbz_magic = b"\xd6\xf5\x1b\x50" + pawnless_tbw_suffix = ".stbw" + pawnless_tbz_suffix = ".stbz" + pawnless_tbw_magic = b"\x7b\xf6\x93\x15" + pawnless_tbz_magic = b"\xe4\xcf\xe7\x23" + + def is_variant_win(self) -> bool: + return not self.occupied_co[self.turn] or self.is_stalemate() + + def is_variant_loss(self) -> bool: + return False + + def is_variant_draw(self) -> bool: + return False + + +class AntichessBoard(GiveawayBoard): + + aliases = ["Antichess", "Anti chess", "Anti"] + uci_variant = "antichess" # Unofficial + starting_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1" + + def __init__(self, fen: Optional[str] = starting_fen, chess960: bool = False) -> None: + super().__init__(fen, chess960=chess960) + + def reset(self) -> None: + super().reset() + self.castling_rights = chess.BB_EMPTY + + +class AtomicBoard(chess.Board): + + aliases = ["Atomic", "Atom", "Atomic chess"] + uci_variant = "atomic" + xboard_variant = "atomic" + + tbw_suffix = ".atbw" + tbz_suffix = ".atbz" + 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) + + def is_variant_win(self) -> bool: + return bool(self.kings and not self.kings & self.occupied_co[not self.turn]) + + def is_variant_loss(self) -> bool: + return bool(self.kings and not self.kings & self.occupied_co[self.turn]) + + def has_insufficient_material(self, color: chess.Color) -> bool: + # Remaining material does not matter if opponent's king is already + # exploded. + if not (self.occupied_co[not color] & self.kings): + return False + + # Bare king can not mate. + if not (self.occupied_co[color] & ~self.kings): + return True + + # As long as the opponent's king is not alone, there is always a chance + # their own pieces explode next to it. + if self.occupied_co[not color] & ~self.kings: + # Unless there are only bishops that cannot explode each other. + if self.occupied == self.bishops | self.kings: + if not (self.bishops & self.occupied_co[chess.WHITE] & chess.BB_DARK_SQUARES): + return not (self.bishops & self.occupied_co[chess.BLACK] & chess.BB_LIGHT_SQUARES) + if not (self.bishops & self.occupied_co[chess.WHITE] & chess.BB_LIGHT_SQUARES): + return not (self.bishops & self.occupied_co[chess.BLACK] & chess.BB_DARK_SQUARES) + return False + + # Queen or pawn (future queen) can give mate against bare king. + if self.queens or self.pawns: + return False + + # Single knight, bishop or rook cannot mate against bare king. + if chess.popcount(self.knights | self.bishops | self.rooks) == 1: + return True + + # Two knights cannot mate against bare king. + if self.occupied == self.knights | self.kings: + return chess.popcount(self.knights) <= 2 + + return False + + def _attacked_for_king(self, path: chess.Bitboard, occupied: chess.Bitboard) -> bool: + # Can castle onto attacked squares if they are connected to the + # enemy king. + enemy_kings = self.kings & self.occupied_co[not self.turn] + for enemy_king in chess.scan_forward(enemy_kings): + path &= ~chess.BB_KING_ATTACKS[enemy_king] + + return super()._attacked_for_king(path, occupied) + + def _kings_connected(self) -> bool: + white_kings = self.kings & self.occupied_co[chess.WHITE] + black_kings = self.kings & self.occupied_co[chess.BLACK] + return any(chess.BB_KING_ATTACKS[sq] & black_kings for sq in chess.scan_forward(white_kings)) + + def _push_capture(self, move: chess.Move, capture_square: chess.Square, piece_type: chess.PieceType, was_promoted: bool) -> None: + explosion_radius = chess.BB_KING_ATTACKS[move.to_square] & ~self.pawns + + # Destroy castling rights. + self.castling_rights &= ~explosion_radius + if explosion_radius & self.kings & self.occupied_co[chess.WHITE] & ~self.promoted: + self.castling_rights &= ~chess.BB_RANK_1 + if explosion_radius & self.kings & self.occupied_co[chess.BLACK] & ~self.promoted: + self.castling_rights &= ~chess.BB_RANK_8 + + # Explode the capturing piece. + self._remove_piece_at(move.to_square) + + # Explode all non pawns around. + for explosion in chess.scan_forward(explosion_radius): + self._remove_piece_at(explosion) + + def checkers_mask(self) -> chess.Bitboard: + return chess.BB_EMPTY if self._kings_connected() else super().checkers_mask() + + def was_into_check(self) -> bool: + return not self._kings_connected() and super().was_into_check() + + def is_into_check(self, move: chess.Move) -> bool: + self.push(move) + was_into_check = self.was_into_check() + self.pop() + return was_into_check + + def is_legal(self, move: chess.Move) -> bool: + if self.is_variant_end(): + return False + + if not self.is_pseudo_legal(move): + return False + + self.push(move) + legal = bool(self.kings) and not self.is_variant_win() and (self.is_variant_loss() or not self.was_into_check()) + self.pop() + + return legal + + def is_stalemate(self) -> bool: + return not self.is_variant_loss() and super().is_stalemate() + + 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 self.generate_pseudo_legal_moves(from_mask, to_mask): + if self.is_legal(move): + yield move + + def status(self) -> chess.Status: + status = super().status() + status &= ~chess.STATUS_OPPOSITE_CHECK + if self.turn == chess.WHITE: + status &= ~chess.STATUS_NO_WHITE_KING + else: + status &= ~chess.STATUS_NO_BLACK_KING + if chess.popcount(self.checkers_mask()) <= 14: + status &= ~chess.STATUS_TOO_MANY_CHECKERS + status &= ~chess.STATUS_IMPOSSIBLE_CHECK + return status + + +class KingOfTheHillBoard(chess.Board): + + aliases = ["King of the Hill", "KOTH", "kingOfTheHill"] + uci_variant = "kingofthehill" + xboard_variant = "kingofthehill" # Unofficial + + tbw_suffix = None + tbz_suffix = None + tbw_magic = None + tbz_magic = None + + def is_variant_end(self) -> bool: + return bool(self.kings & chess.BB_CENTER) + + def is_variant_win(self) -> bool: + return bool(self.kings & self.occupied_co[self.turn] & chess.BB_CENTER) + + def is_variant_loss(self) -> bool: + return bool(self.kings & self.occupied_co[not self.turn] & chess.BB_CENTER) + + def has_insufficient_material(self, color: chess.Color) -> bool: + return False + + +class RacingKingsBoard(chess.Board): + + aliases = ["Racing Kings", "Racing", "Race", "racingkings"] + uci_variant = "racingkings" + xboard_variant = "racingkings" # Unofficial + starting_fen = "8/8/8/8/8/8/krbnNBRK/qrbnNBRQ w - - 0 1" + + tbw_suffix = None + tbz_suffix = None + tbw_magic = None + tbz_magic = None + + def __init__(self, fen: Optional[str] = starting_fen, chess960: bool = False) -> None: + super().__init__(fen, chess960=chess960) + + def reset(self) -> None: + self.set_fen(type(self).starting_fen) + + def is_legal(self, move: chess.Move) -> bool: + 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): + yield move + + def is_variant_end(self) -> bool: + if not self.kings & chess.BB_RANK_8: + return False + + black_kings = self.kings & self.occupied_co[chess.BLACK] + if self.turn == chess.WHITE or black_kings & chess.BB_RANK_8 or not black_kings: + return True + + # 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)) + + def is_variant_draw(self) -> bool: + in_goal = self.kings & chess.BB_RANK_8 + return all(in_goal & side for side in self.occupied_co) + + 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: + 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 + + def status(self) -> chess.Status: + status = super().status() + if self.is_check(): + status |= chess.STATUS_RACE_CHECK | chess.STATUS_TOO_MANY_CHECKERS + 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: + status |= chess.STATUS_RACE_MATERIAL + for color in chess.COLORS: + if chess.popcount(self.occupied_co[color] & self.knights) > 2: + status |= chess.STATUS_RACE_MATERIAL + if chess.popcount(self.occupied_co[color] & self.bishops) > 2: + status |= chess.STATUS_RACE_MATERIAL + if chess.popcount(self.occupied_co[color] & self.rooks) > 2: + status |= chess.STATUS_RACE_MATERIAL + if chess.popcount(self.occupied_co[color] & self.queens) > 1: + status |= chess.STATUS_RACE_MATERIAL + return status + + +class HordeBoard(chess.Board): + + aliases = ["Horde", "Horde chess"] + uci_variant = "horde" + xboard_variant = "horde" # Unofficial + starting_fen = "rnbqkbnr/pppppppp/8/1PP2PP1/PPPPPPPP/PPPPPPPP/PPPPPPPP/PPPPPPPP w kq - 0 1" + + tbw_suffix = None + tbz_suffix = None + tbw_magic = None + tbz_magic = None + + def __init__(self, fen: Optional[str] = starting_fen, chess960: bool = False) -> None: + super().__init__(fen, chess960=chess960) + + def reset(self) -> None: + self.set_fen(type(self).starting_fen) + + def is_variant_end(self) -> bool: + return not all(has_pieces for has_pieces in self.occupied_co) + + def is_variant_draw(self) -> bool: + return not self.occupied + + def is_variant_loss(self) -> bool: + return bool(self.occupied) and not self.occupied_co[self.turn] + + 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: + # 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() + status &= ~chess.STATUS_NO_WHITE_KING + + if chess.popcount(self.occupied_co[chess.WHITE]) <= 36: + status &= ~chess.STATUS_TOO_MANY_WHITE_PIECES + status &= ~chess.STATUS_TOO_MANY_WHITE_PAWNS + + if not self.pawns & chess.BB_RANK_8 and not self.occupied_co[chess.BLACK] & self.pawns & chess.BB_RANK_1: + status &= ~chess.STATUS_PAWNS_ON_BACKRANK + + if self.occupied_co[chess.WHITE] & self.kings: + status |= chess.STATUS_TOO_MANY_KINGS + + return status + + +ThreeCheckBoardT = TypeVar("ThreeCheckBoardT", bound="ThreeCheckBoard") + +class _ThreeCheckBoardState(Generic[ThreeCheckBoardT], chess._BoardState[ThreeCheckBoardT]): + def __init__(self, board: ThreeCheckBoardT) -> None: + super().__init__(board) + 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) + 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", "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" + + tbw_suffix = None + tbz_suffix = None + tbw_magic = None + tbz_magic = None + + def __init__(self, fen: Optional[str] = starting_fen, chess960: bool = False) -> None: + self.remaining_checks = [3, 3] + super().__init__(fen, chess960=chess960) + + def reset_board(self) -> None: + super().reset_board() + self.remaining_checks[chess.WHITE] = 3 + self.remaining_checks[chess.BLACK] = 3 + + def clear_board(self) -> None: + super().clear_board() + 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: + super().push(move) + if self.is_check(): + self.remaining_checks[not self.turn] -= 1 + + def has_insufficient_material(self, color: chess.Color) -> bool: + # Any remaining piece can give check. + return not (self.occupied_co[color] & ~self.kings) + + def set_epd(self, epd: str) -> Dict[str, Union[None, str, int, float, chess.Move, List[chess.Move]]]: + parts = epd.strip().rstrip(";").split(None, 5) + + # Parse ops. + if len(parts) > 5: + operations = self._parse_epd_ops(parts.pop(), lambda: type(self)(" ".join(parts) + " 0 1")) + parts.append(str(operations["hmvc"]) if "hmvc" in operations else "0") + parts.append(str(operations["fmvn"]) if "fmvn" in operations else "1") + self.set_fen(" ".join(parts)) + return operations + else: + self.set_fen(epd) + return {} + + def set_fen(self, fen: str) -> None: + parts = fen.split() + + # Extract check part. + if len(parts) >= 7 and parts[6][0] == "+": + check_part = parts.pop(6) + try: + w, b = check_part[1:].split("+", 1) + wc, bc = 3 - int(w), 3 - int(b) + except ValueError: + raise ValueError(f"invalid check part in lichess three-check fen: {check_part!r}") + elif len(parts) >= 5 and "+" in parts[4]: + check_part = parts.pop(4) + try: + w, b = check_part.split("+", 1) + wc, bc = int(w), int(b) + except ValueError: + raise ValueError(f"invalid check part in three-check fen: {check_part!r}") + else: + wc, bc = 3, 3 + + # Set fen. + super().set_fen(" ".join(parts)) + self.remaining_checks[chess.WHITE] = wc + self.remaining_checks[chess.BLACK] = bc + + 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))] + if operations: + epd.append(self._epd_operations(operations)) + return " ".join(epd) + + def is_variant_end(self) -> bool: + return any(remaining_checks <= 0 for remaining_checks in self.remaining_checks) + + def is_variant_draw(self) -> bool: + return self.remaining_checks[chess.WHITE] <= 0 and self.remaining_checks[chess.BLACK] <= 0 + + def is_variant_loss(self) -> bool: + return self.remaining_checks[not self.turn] <= 0 < self.remaining_checks[self.turn] + + 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: + 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: + board = super().copy(stack=stack) + board.remaining_checks = self.remaining_checks.copy() + return board + + def mirror(self: ThreeCheckBoardT) -> ThreeCheckBoardT: + board = super().mirror() + board.remaining_checks[chess.WHITE] = self.remaining_checks[chess.BLACK] + board.remaining_checks[chess.BLACK] = self.remaining_checks[chess.WHITE] + return board + + +CrazyhouseBoardT = TypeVar("CrazyhouseBoardT", bound="CrazyhouseBoard") + +class _CrazyhouseBoardState(Generic[CrazyhouseBoardT], chess._BoardState[CrazyhouseBoardT]): + def __init__(self, board: CrazyhouseBoardT) -> None: + super().__init__(board) + 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 + board.pockets[chess.BLACK] = self.pockets_b + +CrazyhousePocketT = TypeVar("CrazyhousePocketT", bound="CrazyhousePocket") + +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] = {} + for symbol in symbols: + self.add(chess.PIECE_SYMBOLS.index(symbol)) + + 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 + + def remove(self, piece_type: chess.PieceType) -> None: + """Removes a piece of the given type from this pocket.""" + 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() + + 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()) + + def __repr__(self) -> str: + return f"CrazyhousePocket('{self}')" + + def copy(self: CrazyhousePocketT) -> CrazyhousePocketT: + """Returns a copy of this pocket.""" + pocket = type(self)() + pocket.pieces = copy.copy(self.pieces) + return pocket + +class CrazyhouseBoard(chess.Board): + + aliases = ["Crazyhouse", "Crazy House", "House", "ZH"] + uci_variant = "crazyhouse" + xboard_variant = "crazyhouse" + starting_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 0 1" + + tbw_suffix = None + tbz_suffix = None + tbw_magic = None + tbz_magic = None + + def __init__(self, fen: Optional[str] = starting_fen, chess960: bool = False) -> None: + self.pockets = [CrazyhousePocket(), CrazyhousePocket()] + super().__init__(fen, chess960=chess960) + + def reset_board(self) -> None: + super().reset_board() + self.pockets[chess.WHITE].reset() + self.pockets[chess.BLACK].reset() + + def clear_board(self) -> None: + super().clear_board() + 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: + super().push(move) + if move.drop: + self.pockets[not self.turn].remove(move.drop) + + def _push_capture(self, move: chess.Move, capture_square: chess.Square, piece_type: chess.PieceType, was_promoted: bool) -> None: + if was_promoted: + self.pockets[self.turn].add(chess.PAWN) + else: + self.pockets[self.turn].add(piece_type) + + 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 _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: + king = self.king(self.turn) + if king is None: + return ~self.occupied + + king_attackers = self.attackers_mask(not self.turn, king) + + if not king_attackers: + return ~self.occupied + elif chess.popcount(king_attackers) == 1: + return chess.between(king, chess.msb(king_attackers)) & ~self.occupied + else: + 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: + if move.drop and move.from_square == move.to_square: + return ( + move.drop != chess.KING and + not chess.BB_SQUARES[move.to_square] & self.occupied and + not (move.drop == chess.PAWN and chess.BB_SQUARES[move.to_square] & chess.BB_BACKRANKS) and + self.pockets[self.turn].count(move.drop) > 0) + else: + return super().is_pseudo_legal(move) + + def is_legal(self, move: chess.Move) -> bool: + if move.drop: + return self.is_pseudo_legal(move) and bool(self.legal_drop_squares_mask() & chess.BB_SQUARES[move.to_square]) + else: + 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]): + yield chess.Move(to_square, to_square, drop=pt) + + def generate_legal_drops(self, to_mask: chess.Bitboard = chess.BB_ALL) -> Iterator[chess.Move]: + return self.generate_pseudo_legal_drops(to_mask=self.legal_drop_squares_mask() & to_mask) + + def generate_legal_moves(self, from_mask: chess.Bitboard = chess.BB_ALL, to_mask: chess.Bitboard = chess.BB_ALL) -> Iterator[chess.Move]: + return itertools.chain( + super().generate_legal_moves(from_mask, to_mask), + self.generate_legal_drops(from_mask & to_mask)) + + def parse_san(self, san: str) -> chess.Move: + if "@" in san: + 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()}") + return move + else: + return super().parse_san(san) + + def has_insufficient_material(self, color: chess.Color) -> bool: + # 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.pawns and + not self.rooks and + not self.queens and + not any(pocket.count(chess.PAWN) for pocket in self.pockets) and + not any(pocket.count(chess.ROOK) for pocket in self.pockets) and + not any(pocket.count(chess.QUEEN) for pocket in self.pockets)) + + def set_fen(self, fen: str) -> None: + position_part, info_part = fen.split(None, 1) + + # Transform to lichess-style ZH FEN. + if position_part.endswith("]"): + if position_part.count("/") != 7: + raise ValueError(f"expected 8 rows in position part of zh fen: {fen!r}") + position_part = position_part[:-1].replace("[", "/", 1) + + # Split off pocket part. + if position_part.count("/") == 8: + position_part, pocket_part = position_part.rsplit("/", 1) + else: + pocket_part = "" + + # Parse pocket. + white_pocket = CrazyhousePocket(c.lower() for c in pocket_part if c.isupper()) + black_pocket = CrazyhousePocket(c for c in pocket_part if not c.isupper()) + + # Set FEN and pockets. + super().set_fen(position_part + " " + info_part) + 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: 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: + board = super().copy(stack=stack) + board.pockets[chess.WHITE] = self.pockets[chess.WHITE].copy() + board.pockets[chess.BLACK] = self.pockets[chess.BLACK].copy() + return board + + def mirror(self: CrazyhouseBoardT) -> CrazyhouseBoardT: + board = super().mirror() + board.pockets[chess.WHITE] = self.pockets[chess.BLACK].copy() + board.pockets[chess.BLACK] = self.pockets[chess.WHITE].copy() + return board + + def status(self) -> chess.Status: + status = super().status() + + if chess.popcount(self.pawns) + self.pockets[chess.WHITE].count(chess.PAWN) + self.pockets[chess.BLACK].count(chess.PAWN) <= 16: + status &= ~chess.STATUS_TOO_MANY_BLACK_PAWNS + status &= ~chess.STATUS_TOO_MANY_WHITE_PAWNS + + if chess.popcount(self.occupied) + len(self.pockets[chess.WHITE]) + len(self.pockets[chess.BLACK]) <= 32: + status &= ~chess.STATUS_TOO_MANY_BLACK_PIECES + status &= ~chess.STATUS_TOO_MANY_WHITE_PIECES + + return status + + +VARIANTS: List[Type[chess.Board]] = [ + chess.Board, + SuicideBoard, GiveawayBoard, AntichessBoard, + AtomicBoard, + KingOfTheHillBoard, + RacingKingsBoard, + HordeBoard, + ThreeCheckBoard, + CrazyhouseBoard, +] + + +def find_variant(name: str) -> Type[chess.Board]: + """ + 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 + raise ValueError(f"unsupported variant: {name}") diff --git a/cartesi-python-chess-cartesi-img/python-chess-validate.py b/cartesi-python-chess-cartesi-img/python-chess-validate.py new file mode 100755 index 0000000..de1c369 --- /dev/null +++ b/cartesi-python-chess-cartesi-img/python-chess-validate.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 + +import chess.pgn +import logging + +logging.getLogger("chess.pgn").setLevel(logging.CRITICAL) + +pgn = open("/mnt/cartesi-python-chess/chess-game.pgn") +game = chess.pgn.read_game(pgn) +game.errors + +print(("Errors:"), game.errors) + diff --git a/cartesi-python-chess-cartesi-img/python-chess-validate.pyc b/cartesi-python-chess-cartesi-img/python-chess-validate.pyc new file mode 100644 index 0000000..8d37119 Binary files /dev/null and b/cartesi-python-chess-cartesi-img/python-chess-validate.pyc differ diff --git a/do-all-cartesi-python-chess b/do-all-cartesi-python-chess new file mode 100755 index 0000000..2b44e37 --- /dev/null +++ b/do-all-cartesi-python-chess @@ -0,0 +1,8 @@ +#!/bin/bash +set -x + +./edit-rootfs-cartesi-cartesi-python-chess +./edit-mntfs-cartesi-cartesi-python-chess +./build-cartesi-cartesi-python-chess +./run-cartesi-cartesi-python-chess + diff --git a/edit-mntfs-cartesi-cartesi-python-chess b/edit-mntfs-cartesi-cartesi-python-chess new file mode 100755 index 0000000..2f07797 --- /dev/null +++ b/edit-mntfs-cartesi-cartesi-python-chess @@ -0,0 +1,6 @@ +#!/bin/bash + +set -x + +vim cartesi-python-chess-cartesi-img/cartesi-python-chess.sh + diff --git a/edit-rootfs-cartesi-cartesi-python-chess b/edit-rootfs-cartesi-cartesi-python-chess new file mode 100755 index 0000000..6c02ad0 --- /dev/null +++ b/edit-rootfs-cartesi-cartesi-python-chess @@ -0,0 +1,17 @@ +#!/bin/bash + +set -x + +MYDIR=`pwd` + +cd /home/jebba/devel/cartesi/machine-emulator-sdk/fs + +make config + +cp -p ~/devel/cartesi/machine-emulator-sdk/fs/cartesi-buildroot-config \ + $MYDIR/ + +cp -p /home/jebba/devel/cartesi/machine-emulator-sdk/fs/rootfs.ext2 \ + /opt/cartesi/share/images/rootfs-cartesi-python-chess.ext2 + + diff --git a/run-cartesi-cartesi-python-chess b/run-cartesi-cartesi-python-chess new file mode 100755 index 0000000..e155bba --- /dev/null +++ b/run-cartesi-cartesi-python-chess @@ -0,0 +1,9 @@ +#!/bin/bash + +# Launch virtual machine with pre-built root image with cartesi-python-chess +# and cartesi-python-chess.sh script in new image that gets run at boot +cartesi-machine \ + --flash-drive=label:cartesi-python-chess,filename:cartesi-python-chess.ext2 \ + --flash-drive=label:root,filename:/opt/cartesi/share/images/rootfs-cartesi-python-chess.ext2 \ + -- /mnt/cartesi-python-chess/cartesi-python-chess.sh +