From: Fabian Fichter Date: Sat, 4 Apr 2020 21:33:56 +0000 (+0200) Subject: Merge python wrapper X-Git-Url: http://winboard.nl/cgi-bin?a=commitdiff_plain;h=e810955843f496a57c4a08c5f30d639900dbb860;p=fairystockfish.git Merge python wrapper Include python wrapper by gbtami in main repository. Closes #93. --- diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1265c81 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include AUTHORS Copying.txt Readme.md test.py +recursive-include src *.h *.cpp diff --git a/Readme.md b/Readme.md index d6f012c..e89ad34 100644 --- a/Readme.md +++ b/Readme.md @@ -4,6 +4,7 @@ [![Build Status](https://travis-ci.org/ianfab/Fairy-Stockfish.svg?branch=master)](https://travis-ci.org/ianfab/Fairy-Stockfish) [![Build Status](https://ci.appveyor.com/api/projects/status/github/ianfab/Fairy-Stockfish?branch=master&svg=true)](https://ci.appveyor.com/project/ianfab/Fairy-Stockfish/branch/master) +[![PyPI version](https://badge.fury.io/py/pyffish.svg)](https://badge.fury.io/py/pyffish) Fairy-Stockfish is a chess variant engine derived from [Stockfish](https://github.com/official-stockfish/Stockfish/) designed for the support of fairy chess variants and easy extensibility with more games. It can play various historical, regional, and modern chess variants as well as [games with user-defined rules](https://github.com/ianfab/Fairy-Stockfish/wiki/Variant-configuration). For [compatibility with graphical user interfaces](https://github.com/ianfab/Fairy-Stockfish/wiki/Usage) it supports the UCI, UCCI, USI, and CECP/XBoard protocols. diff --git a/appveyor_python.yml b/appveyor_python.yml new file mode 100644 index 0000000..7bc6d5e --- /dev/null +++ b/appveyor_python.yml @@ -0,0 +1,50 @@ +environment: + + matrix: + - PYTHON: "C:\\Python36-x64" + - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python38-x64" + +init: + - "ECHO %PYTHON% %PYTHON_ARCH% %MSVC_VERSION%" + +install: + - ps: | + if (-not (Test-Path $env:PYTHON)) { + curl -o install_python.ps1 https://raw.githubusercontent.com/matthew-brett/multibuild/11a389d78892cf90addac8f69433d5e22bfa422a/install_python.ps1 + .\install_python.ps1 + } + - ps: if (-not (Test-Path $env:PYTHON)) { throw "No $env:PYTHON" } + - "set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - python --version + + # We need wheel installed to build wheels + - "%PYTHON%\\python.exe -m pip install wheel" + +build: off + +test_script: + # Put your test command here. + # If you don't need to build C extensions on 64-bit Python 3.3 or 3.4, + # you can remove "build.cmd" from the front of the command, as it's + # only needed to support those cases. + # Note that you must use the environment variable %PYTHON% to refer to + # the interpreter you're using - Appveyor does not do anything special + # to put the Python version you want to use on PATH. + - "%PYTHON%\\python.exe setup.py test" + +after_test: + # This step builds your wheels. + # Again, you only need build.cmd if you're building C extensions for + # 64-bit Python 3.3/3.4. And you need to use %PYTHON% to get the correct + # interpreter + - "%PYTHON%\\python.exe setup.py bdist_wheel" + +artifacts: + # bdist_wheel puts your built wheel in the dist directory + - path: dist\* + +#on_success: +# You can use this step to upload your artifacts to a public website. +# See Appveyor's documentation for more details. Or you can simply +# access your wheels from the Appveyor "artifacts" tab for your build. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..718bc33 --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +from setuptools import setup, Extension +from glob import glob +import platform +import io + + +args = ["-DLARGEBOARDS", "-DPRECOMPUTED_MAGICS", "-flto", "-std=c++11"] + +if not platform.python_compiler().startswith("MSC"): + args.append("-Wno-date-time") + +if "64bit" in platform.architecture(): + args.append("-DIS_64BIT") + +CLASSIFIERS = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] + +with io.open("Readme.md", "r", encoding="utf8") as fh: + long_description = fh.read().strip() + +pyffish_module = Extension( + "pyffish", + sources=glob("src/*.cpp") + glob("src/syzygy/*.cpp"), + extra_compile_args=args) + +setup(name="pyffish", version="0.0.44", + description="Fairy-Stockfish Python wrapper", + long_description=long_description, + long_description_content_type="text/markdown", + author="Bajusz Tamás", + author_email="gbtami@gmail.com", + license="GPL3", + classifiers=CLASSIFIERS, + url="https://github.com/gbtami/Fairy-Stockfish", + python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", + ext_modules=[pyffish_module] + ) diff --git a/src/pyffish.cpp b/src/pyffish.cpp new file mode 100644 index 0000000..4625c69 --- /dev/null +++ b/src/pyffish.cpp @@ -0,0 +1,483 @@ +/* + Based on Jean-Francois Romang work + https://github.com/jromang/Stockfish/blob/pyfish/src/pyfish.cpp +*/ + +#include + +#include "misc.h" +#include "types.h" +#include "bitboard.h" +#include "evaluate.h" +#include "position.h" +#include "search.h" +#include "syzygy/tbprobe.h" +#include "thread.h" +#include "tt.h" +#include "uci.h" +#include "piece.h" +#include "variant.h" + +static PyObject* PyFFishError; + +namespace PSQT { + void init(const Variant* v); +} + +using namespace std; + +namespace +{ + +const string move_to_san(Position& pos, Move m, const bool shogi) { + Bitboard others, b; + string san; + Color us = pos.side_to_move(); + Square from = from_sq(m); + Square to = to_sq(m); + Piece pc = pos.piece_on(from); + PieceType pt = type_of(pc); + + string protocol = Options["Protocol"]; + Options["Protocol"] = string(shogi ? "usi" : "uci"); + + if (type_of(m) == CASTLING) + { + san = to > from ? "O-O" : "O-O-O"; + + if (is_gating(m)) + { + san += string("/") + pos.piece_to_char()[make_piece(WHITE, gating_type(m))]; + san += gating_square(m) == to ? UCI::square(pos, to) : UCI::square(pos, from); + } + } + else + { + if (type_of(m) == DROP) + san += UCI::dropped_piece(pos, m) + (shogi ? '*' : '@'); + else + { + if (pt != PAWN) + { + if (pos.is_promoted(from) && shogi) + san += "+" + string(1, toupper(pos.piece_to_char()[pos.unpromoted_piece_on(from)])); + else if (pos.piece_to_char_synonyms()[make_piece(WHITE, pt)] != ' ') + san += pos.piece_to_char_synonyms()[make_piece(WHITE, pt)]; + else + san += pos.piece_to_char()[make_piece(WHITE, pt)]; + + // A disambiguation occurs if we have more then one piece of type 'pt' + // that can reach 'to' with a legal move. + others = b = ((pos.capture(m) ? attacks_bb(~us, pt == HORSE ? KNIGHT : pt, to, pos.pieces()) + : moves_bb(~us, pt == HORSE ? KNIGHT : pt, to, pos.pieces())) & pos.pieces(us, pt)) ^ from; + + while (b) + { + Square s = pop_lsb(&b); + if ( !pos.pseudo_legal(make_move(s, to)) + || !pos.legal(make_move(s, to)) + || (shogi && pos.unpromoted_piece_on(s) != pos.unpromoted_piece_on(from))) + others ^= s; + } + + if (!others) + { /* disambiguation is not needed */ } + else if (!(others & file_bb(from)) && !shogi) + san += UCI::square(pos, from)[0]; + else if (!(others & rank_bb(from)) && !shogi) + san += UCI::square(pos, from)[1]; + else + san += UCI::square(pos, from); + } + + if (pos.capture(m) && from != to) + { + if (pt == PAWN) + san += UCI::square(pos, from)[0]; + san += 'x'; + } + else if (shogi) + san += '-'; + } + + san += UCI::square(pos, to); + + if (type_of(m) == PROMOTION) + san += string("=") + pos.piece_to_char()[make_piece(WHITE, promotion_type(m))]; + else if (type_of(m) == PIECE_PROMOTION) + san += string("+"); + else if (type_of(m) == PIECE_DEMOTION) + san += string("-"); + else if (type_of(m) == NORMAL && shogi && pos.pseudo_legal(make(from, to))) + san += string("="); + else if (is_gating(m)) + san += string("/") + pos.piece_to_char()[make_piece(WHITE, gating_type(m))]; + } + + if (pos.gives_check(m) && (!shogi)) + { + StateInfo st; + pos.do_move(m, st); + san += MoveList(pos).size() ? "+" : "#"; + pos.undo_move(m); + } + // reset protocol + Options["Protocol"] = protocol; + return san; +} + +bool hasInsufficientMaterial(Color c, const Position& pos) { + + if (pos.captures_to_hand() || pos.count_in_hand(c, ALL_PIECES)) + return false; + + for (PieceType pt : { ROOK, QUEEN, ARCHBISHOP, CHANCELLOR, SILVER }) + if (pos.count(c, pt) || (pos.count(c, PAWN) && pos.promotion_piece_types().find(pt) != pos.promotion_piece_types().end())) + return false; + + // To avoid false positives, treat pawn + anything as sufficient mating material. + // This is too conservative for South-East Asian variants. + if (pos.count(c, PAWN) && pos.count() >= 4) + return false; + + if (pos.count(c, KNIGHT) >= 2 || (pos.count(c, KNIGHT) && (pos.count(c, BISHOP) || pos.count(c, FERS) || pos.count(c, FERS_ALFIL)))) + return false; + + // Check for opposite colored color-bound pieces + if ( (pos.count(c, BISHOP) || pos.count(c, FERS_ALFIL)) + && (DarkSquares & pos.pieces(BISHOP, FERS_ALFIL)) && (~DarkSquares & pos.pieces(BISHOP, FERS_ALFIL))) + return false; + + if (pos.count(c, FERS) && (DarkSquares & pos.pieces(FERS)) && (~DarkSquares & pos.pieces(FERS))) + return false; + + if (pos.pieces(c, CANNON, JANGGI_CANNON) && (pos.count(c, ALL_PIECES) > 2 || pos.count(~c, ALL_PIECES) > 1)) + return false; + + if (pos.count(c, JANGGI_ELEPHANT) >= 2) + return false; + + // Pieces sufficient for stalemate (Xiangqi) + if (pos.stalemate_value() != VALUE_DRAW) + for (PieceType pt : { HORSE, SOLDIER, JANGGI_ELEPHANT }) + if (pos.count(c, pt)) + return false; + + return true; +} + +void buildPosition(Position& pos, StateListPtr& states, const char *variant, const char *fen, PyObject *moveList, const bool chess960) { + states = StateListPtr(new std::deque(1)); // Drop old and create a new one + + const Variant* v = variants.find(string(variant))->second; + if (strcmp(fen, "startpos") == 0) + fen = v->startFen.c_str(); + bool sfen = false; + Options["Protocol"] = string("uci"); + Options["UCI_Chess960"] = (chess960) ? UCI::Option(true) : UCI::Option(false); + pos.set(v, string(fen), Options["UCI_Chess960"], &states->back(), Threads.main(), sfen); + + // parse move list + int numMoves = PyList_Size(moveList); + for (int i=0; iemplace_back(); + pos.do_move(m, states->back()); + } + else + { + PyErr_SetString(PyExc_ValueError, (string("Invalid move '")+moveStr+"'").c_str()); + } + } + return; +} + +} + +extern "C" PyObject* pyffish_info(PyObject* self) { + return Py_BuildValue("s", engine_info().c_str()); +} + +// INPUT option name, option value +extern "C" PyObject* pyffish_setOption(PyObject* self, PyObject *args) { + const char *name; + PyObject *valueObj; + if (!PyArg_ParseTuple(args, "sO", &name, &valueObj)) return NULL; + + if (Options.count(name)) + Options[name] = string(PyBytes_AS_STRING(PyUnicode_AsEncodedString(PyObject_Str(valueObj), "UTF-8", "strict"))); + else + { + PyErr_SetString(PyExc_ValueError, (string("No such option ")+name+"'").c_str()); + return NULL; + } + Py_RETURN_NONE; +} + +// INPUT variant +extern "C" PyObject* pyffish_startFen(PyObject* self, PyObject *args) { + const char *variant; + + if (!PyArg_ParseTuple(args, "s", &variant)) { + return NULL; + } + + return Py_BuildValue("s", variants.find(string(variant))->second->startFen.c_str()); +} + +// INPUT variant +extern "C" PyObject* pyffish_twoBoards(PyObject* self, PyObject *args) { + const char *variant; + + if (!PyArg_ParseTuple(args, "s", &variant)) { + return NULL; + } + + return Py_BuildValue("O", variants.find(string(variant))->second->twoBoards ? Py_True : Py_False); +} + +// INPUT variant, fen, move +extern "C" PyObject* pyffish_getSAN(PyObject* self, PyObject *args) { + PyObject* moveList = PyList_New(0); + Position pos; + const char *fen, *variant, *move; + + int chess960 = false; + if (!PyArg_ParseTuple(args, "sss|p", &variant, &fen, &move, &chess960)) { + return NULL; + } + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, moveList, chess960); + string moveStr = move; + const Variant* v = variants.find(string(variant))->second; + const bool shogi = v->variantTemplate == "shogi"; + return Py_BuildValue("s", move_to_san(pos, UCI::to_move(pos, moveStr), shogi).c_str()); +} + +// INPUT variant, fen, movelist +extern "C" PyObject* pyffish_getSANmoves(PyObject* self, PyObject *args) { + PyObject* sanMoves = PyList_New(0), *moveList; + Position pos; + const char *fen, *variant; + + int chess960 = false; + if (!PyArg_ParseTuple(args, "ssO!|p", &variant, &fen, &PyList_Type, &moveList, &chess960)) { + return NULL; + } + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, sanMoves, chess960); + + const Variant* v = variants.find(string(variant))->second; + const bool shogi = v->variantTemplate == "shogi"; + + int numMoves = PyList_Size(moveList); + for (int i=0; iemplace_back(); + pos.do_move(m, states->back()); + } + else + { + PyErr_SetString(PyExc_ValueError, (string("Invalid move '")+moveStr+"'").c_str()); + return NULL; + } + } + return sanMoves; +} + +// INPUT variant, fen, move list +extern "C" PyObject* pyffish_legalMoves(PyObject* self, PyObject *args) { + PyObject* legalMoves = PyList_New(0), *moveList; + Position pos; + const char *fen, *variant; + + int chess960 = false; + if (!PyArg_ParseTuple(args, "ssO!|p", &variant, &fen, &PyList_Type, &moveList, &chess960)) { + return NULL; + } + + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, moveList, chess960); + for (const auto& m : MoveList(pos)) + { + PyObject *moveStr; + moveStr=Py_BuildValue("s", UCI::move(pos, m).c_str()); + PyList_Append(legalMoves, moveStr); + Py_XDECREF(moveStr); + } + return legalMoves; +} + +// INPUT variant, fen, move list +extern "C" PyObject* pyffish_getFEN(PyObject* self, PyObject *args) { + PyObject *moveList; + Position pos; + const char *fen, *variant; + + int chess960 = false, sfen = false, showPromoted = false; + if (!PyArg_ParseTuple(args, "ssO!|ppp", &variant, &fen, &PyList_Type, &moveList, &chess960, &sfen, &showPromoted)) { + return NULL; + } + + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, moveList, chess960); + return Py_BuildValue("s", pos.fen(sfen, showPromoted).c_str()); +} + +// INPUT variant, fen, move list +extern "C" PyObject* pyffish_givesCheck(PyObject* self, PyObject *args) { + PyObject *moveList; + Position pos; + const char *fen, *variant; + int chess960 = false; + if (!PyArg_ParseTuple(args, "ssO!|p", &variant, &fen, &PyList_Type, &moveList, &chess960)) { + return NULL; + } + + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, moveList, chess960); + return Py_BuildValue("O", pos.checkers() ? Py_True : Py_False); +} + +// INPUT variant, fen, move list +// should only be called when the move list is empty +extern "C" PyObject* pyffish_gameResult(PyObject* self, PyObject *args) { + PyObject *moveList; + Position pos; + const char *fen, *variant; + bool gameEnd; + Value result; + int chess960 = false; + if (!PyArg_ParseTuple(args, "ssO!|p", &variant, &fen, &PyList_Type, &moveList, &chess960)) { + return NULL; + } + + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, moveList, chess960); + assert(!MoveList(pos).size()); + gameEnd = pos.is_immediate_game_end(result); + if (!gameEnd) + result = pos.checkers() ? pos.checkmate_value() : pos.stalemate_value(); + + return Py_BuildValue("i", result); +} + +// INPUT variant, fen, move list +extern "C" PyObject* pyffish_isImmediateGameEnd(PyObject* self, PyObject *args) { + PyObject *moveList; + Position pos; + const char *fen, *variant; + bool gameEnd; + Value result; + int chess960 = false; + if (!PyArg_ParseTuple(args, "ssO!|p", &variant, &fen, &PyList_Type, &moveList, &chess960)) { + return NULL; + } + + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, moveList, chess960); + gameEnd = pos.is_immediate_game_end(result); + return Py_BuildValue("(Oi)", gameEnd ? Py_True : Py_False, result); +} + +// INPUT variant, fen, move list +extern "C" PyObject* pyffish_isOptionalGameEnd(PyObject* self, PyObject *args) { + PyObject *moveList; + Position pos; + const char *fen, *variant; + bool gameEnd; + Value result; + int chess960 = false; + if (!PyArg_ParseTuple(args, "ssO!|p", &variant, &fen, &PyList_Type, &moveList, &chess960)) { + return NULL; + } + + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, moveList, chess960); + gameEnd = pos.is_optional_game_end(result); + return Py_BuildValue("(Oi)", gameEnd ? Py_True : Py_False, result); +} + +// INPUT variant, fen, move list +extern "C" PyObject* pyffish_hasInsufficientMaterial(PyObject* self, PyObject *args) { + PyObject *moveList; + Position pos; + const char *fen, *variant; + int chess960 = false; + if (!PyArg_ParseTuple(args, "ssO!|p", &variant, &fen, &PyList_Type, &moveList, &chess960)) { + return NULL; + } + + StateListPtr states(new std::deque(1)); + buildPosition(pos, states, variant, fen, moveList, chess960); + + bool wInsufficient = hasInsufficientMaterial(WHITE, pos); + bool bInsufficient = hasInsufficientMaterial(BLACK, pos); + + return Py_BuildValue("(OO)", wInsufficient ? Py_True : Py_False, bInsufficient ? Py_True : Py_False); +} + + +static PyMethodDef PyFFishMethods[] = { + {"info", (PyCFunction)pyffish_info, METH_NOARGS, "Get Stockfish version info."}, + {"set_option", (PyCFunction)pyffish_setOption, METH_VARARGS, "Set UCI option."}, + {"start_fen", (PyCFunction)pyffish_startFen, METH_VARARGS, "Get starting position FEN."}, + {"two_boards", (PyCFunction)pyffish_twoBoards, METH_VARARGS, "Checks whether the variant is played on two boards."}, + {"get_san", (PyCFunction)pyffish_getSAN, METH_VARARGS, "Get SAN move from given FEN and UCI move."}, + {"get_san_moves", (PyCFunction)pyffish_getSANmoves, METH_VARARGS, "Get SAN movelist from given FEN and UCI movelist."}, + {"legal_moves", (PyCFunction)pyffish_legalMoves, METH_VARARGS, "Get legal moves from given FEN and movelist."}, + {"get_fen", (PyCFunction)pyffish_getFEN, METH_VARARGS, "Get resulting FEN from given FEN and movelist."}, + {"gives_check", (PyCFunction)pyffish_givesCheck, METH_VARARGS, "Get check status from given FEN and movelist."}, + {"game_result", (PyCFunction)pyffish_gameResult, METH_VARARGS, "Get result from given FEN, considering variant end, checkmate, and stalemate."}, + {"is_immediate_game_end", (PyCFunction)pyffish_isImmediateGameEnd, METH_VARARGS, "Get result from given FEN if variant rules ends the game."}, + {"is_optional_game_end", (PyCFunction)pyffish_isOptionalGameEnd, METH_VARARGS, "Get result from given FEN it rules enable game end by player."}, + {"has_insufficient_material", (PyCFunction)pyffish_hasInsufficientMaterial, METH_VARARGS, "Checks for insufficient material."}, + {NULL, NULL, 0, NULL}, // sentinel +}; + +static PyModuleDef pyffishmodule = { + PyModuleDef_HEAD_INIT, + "pyffish", + "Fairy-Stockfish extension module.", + -1, + PyFFishMethods, +}; + +PyMODINIT_FUNC PyInit_pyffish() { + PyObject* module; + + module = PyModule_Create(&pyffishmodule); + if (module == NULL) { + return NULL; + } + PyFFishError = PyErr_NewException("pyffish.error", NULL, NULL); + Py_INCREF(PyFFishError); + PyModule_AddObject(module, "error", PyFFishError); + + // initialize stockfish + pieceMap.init(); + variants.init(); + UCI::init(Options); + PSQT::init(variants.find("chess")->second); + Bitboards::init(); + Position::init(); + Bitbases::init(); + Search::init(); + Threads.set(Options["Threads"]); + Search::clear(); // After threads are up + + return module; +}; diff --git a/test.py b/test.py new file mode 100644 index 0000000..bba06ad --- /dev/null +++ b/test.py @@ -0,0 +1,386 @@ +# -*- coding: utf-8 -*- + +import faulthandler +import unittest +import pyffish as sf + +faulthandler.enable() + +CHESS = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" +CHESS960 = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w HAha - 0 1" +CAPA = "rnabqkbcnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNABQKBCNR w KQkq - 0 1" +CAPAHOUSE = "rnabqkbcnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNABQKBCNR[] w KQkq - 0 1" +SITTUYIN = "8/8/4pppp/pppp4/4PPPP/PPPP4/8/8[KFRRSSNNkfrrssnn] w - - 0 1" +MAKRUK = "rnsmksnr/8/pppppppp/8/8/PPPPPPPP/8/RNSKMSNR w - - 0 1" +SHOGI = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[-] w - - 0 1" +SHOGI_SFEN = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1" +SEIRAWAN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[HEhe] w KQBCDFGkqbcdfg - 0 1" +GRAND = "r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R w - - 0 1" +GRANDHOUSE = "r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R[] w - - 0 1" +XIANGQI = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1" +SHOGUN = "rnb+fkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB+FKBNR w KQkq - 0 1" +JANGGI = "rnba1abnr/4k4/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/4K4/RNBA1ABNR w - - 0 1" + + +ini_text = """ +# Hybrid variant of Grand-chess and crazyhouse, using Grand-chess as a template +[grandhouse:grand] +startFen = r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R[] w - - 0 1 +pieceDrops = true +capturesToHand = true + +# Hybrid variant of Gothic-chess and crazyhouse, using Capablanca as a template +[gothhouse:capablanca] +startFen = rnbqckabnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNBQCKABNR[] w KQkq - 0 1 +pieceDrops = true +capturesToHand = true + +# Shogun chess +[shogun:crazyhouse] +startFen = rnb+fkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB+FKBNR w KQkq - 0 1 +commoner = c +centaur = g +archbishop = a +chancellor = m +fers = f +promotionRank = 6 +promotionLimit = g:1 a:1 m:1 q:1 +promotionPieceTypes = - +promotedPieceType = p:c n:g b:a r:m f:q +mandatoryPawnPromotion = false +firstRankPawnDrops = true +promotionZonePawnDrops = true +whiteDropRegion = *1 *2 *3 *4 *5 +blackDropRegion = *4 *5 *6 *7 *8 +immobilityIllegal = true""" + +print(ini_text, file=open("variants.ini", "w")) +sf.set_option("VariantPath", "variants.ini") + + +variant_positions = { + "chess": { + "k7/8/8/8/8/8/8/K7 w - - 0 1": (True, True), # K vs K + "k7/b7/8/8/8/8/8/K7 w - - 0 1": (True, True), # K vs KB + "k7/n7/8/8/8/8/8/K7 w - - 0 1": (True, True), # K vs KN + "k7/p7/8/8/8/8/8/K7 w - - 0 1": (True, False), # K vs KP + "k7/r7/8/8/8/8/8/K7 w - - 0 1": (True, False), # K vs KR + "k7/q7/8/8/8/8/8/K7 w - - 0 1": (True, False), # K vs KQ + "k7/nn6/8/8/8/8/8/K7 w - - 0 1": (True, False), # K vsNN K + "k7/bb6/8/8/8/8/8/K7 w - - 0 1": (True, False), # K vs KBB opp color + "k7/b1b5/8/8/8/8/8/K7 w - - 0 1": (True, True), # K vs KBB same color + "kb6/8/8/8/8/8/8/K1B6 w - - 0 1": (True, True), # KB vs KB same color + "kb6/8/8/8/8/8/8/KB7 w - - 0 1": (False, False), # KB vs KB opp color + }, + "seirawan": { + "k7/8/8/8/8/8/8/K7[] w - - 0 1": (True, True), # K vs K + "k7/8/8/8/8/8/8/KH6[] w - - 0 1": (False, True), # KH vs K + "k7/8/8/8/8/8/8/4K3[E] w E - 0 1": (False, True), # KE vs K + }, + "sittuyin": { + "8/8/4pppp/pppp4/4PPPP/PPPP4/8/8[KFRRSSNNkfrrssnn] w - - 0 1": (False, False), # starting position + "k7/8/8/8/8/8/8/K7[] w - - 0 1": (True, True), # K vs K + "k6P/8/8/8/8/8/8/K7[] w - - 0 1": (True, True), # KP vs K + "k6P/8/8/8/8/8/8/K6p[] w - - 0 1": (False, False), # KP vs KP + "k7/8/8/8/8/8/8/KFF5[] w - - 0 1": (False, True), # KFF vs K + "k7/8/8/8/8/8/8/KS6[] w - - 0 1": (False, True), # KS vs K + }, + "xiangqi": { + "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1": (False, False), # starting position + "5k3/4a4/3CN4/9/1PP5p/9/8P/4C4/4A4/2B1K4 w - - 0 46": (False, False), # issue #53 + "4k4/9/9/9/9/9/9/9/9/4K4 w - - 0 1": (True, True), # K vs K + "4k4/9/9/4p4/9/9/9/9/9/4KR3 w - - 0 1": (False, False), # KR vs KP + "4k4/9/9/9/9/9/9/9/9/3KN4 w - - 0 1": (False, True), # KN vs K + "4k4/9/4b4/9/9/9/9/4B4/9/4K4 w - - 0 1": (True, True), # KB vs KB + "4k4/9/9/9/9/9/9/9/4A4/4KC3 w - - 0 1": (False, True), # KCA vs K + }, + "janggi": { + JANGGI: (False, False), # starting position + "5k3/4a4/3CN4/9/1PP5p/9/8P/4C4/4A4/2B1K4 w - - 0 46": (False, False), # issue #53 + "4k4/9/9/9/9/4B4/4B4/9/9/4K4 w - - 0 1": (False, True), # KEE vs K + "4k4/9/9/9/9/9/9/9/4A4/4KC3 w - - 0 1": (False, True), # KCA vs K + }, + "shako": { + "k9/10/10/10/10/10/10/10/10/KC8 w - - 0 1": (True, True), # KC vs K + "k9/10/10/10/10/10/10/10/10/KCC7 w - - 0 1": (False, True), # KCC vs K + "k9/10/10/10/10/10/10/10/10/KEC7 w - - 0 1": (False, True), # KEC vs K + "k9/10/10/10/10/10/10/10/10/KNE7 w - - 0 1": (False, True), # KNE vs K + "kb8/10/10/10/10/10/10/10/10/KE8 w - - 0 1": (False, False), # KE vs KB opp color + "kb8/10/10/10/10/10/10/10/10/K1E7 w - - 0 1": (True, True), # KE vs KB same color + } +} + + +class TestPyffish(unittest.TestCase): + def test_info(self): + result = sf.info() + self.assertEqual(result[:15], "Fairy-Stockfish") + + def test_set_option(self): + result = sf.set_option("UCI_Variant", "capablanca") + self.assertIsNone(result) + + def test_two_boards(self): + self.assertFalse(sf.two_boards("chess")) + self.assertTrue(sf.two_boards("bughouse")) + + def test_start_fen(self): + result = sf.start_fen("capablanca") + self.assertEqual(result, CAPA) + + result = sf.start_fen("capahouse") + self.assertEqual(result, CAPAHOUSE) + + result = sf.start_fen("xiangqi") + self.assertEqual(result, XIANGQI) + + result = sf.start_fen("grandhouse") + self.assertEqual(result, GRANDHOUSE) + + result = sf.start_fen("shogun") + self.assertEqual(result, SHOGUN) + + def test_legal_moves(self): + fen = "10/10/10/10/10/k9/10/K9 w - - 0 1" + result = sf.legal_moves("capablanca", fen, []) + self.assertEqual(result, ["a1b1"]) + + result = sf.legal_moves("grand", GRAND, ["a3a5"]) + self.assertIn("a10b10", result) + + result = sf.legal_moves("xiangqi", XIANGQI, ["h3h10"]) + self.assertIn("i10h10", result) + + result = sf.legal_moves("xiangqi", XIANGQI, ["h3h10"]) + self.assertIn("i10h10", result) + + result = sf.legal_moves("shogun", SHOGUN, ["c2c4", "b8c6", "b2b4", "b7b5", "c4b5", "c6b8"]) + self.assertIn("b5b6+", result) + + # In Janggi stalemate position pass move (in place king move) is possible + fen = "4k4/c7R/9/3R1R3/9/9/9/9/9/3K5 b - - 0 1" + result = sf.legal_moves("janggi", fen, []) + self.assertEqual(result, ["e10e10"]) + + def test_short_castling(self): + legals = ['f5f4', 'a7a6', 'b7b6', 'c7c6', 'd7d6', 'e7e6', 'i7i6', 'j7j6', 'a7a5', 'b7b5', 'c7c5', 'e7e5', 'i7i5', 'j7j5', 'b8a6', 'b8c6', 'h6g4', 'h6i4', 'h6j5', 'h6f7', 'h6g8', 'h6i8', 'd5a2', 'd5b3', 'd5f3', 'd5c4', 'd5e4', 'd5c6', 'd5e6', 'd5f7', 'd5g8', 'j8g8', 'j8h8', 'j8i8', 'e8f7', 'c8b6', 'c8d6', 'g6g2', 'g6g3', 'g6f4', 'g6g4', 'g6h4', 'g6e5', 'g6g5', 'g6i5', 'g6a6', 'g6b6', 'g6c6', 'g6d6', 'g6e6', 'g6f6', 'g6h8', 'f8f7', 'f8g8', 'f8i8'] + moves = ['b2b4', 'f7f5', 'c2c3', 'g8d5', 'a2a4', 'h8g6', 'f2f3', 'i8h6', 'h2h3'] + result = sf.legal_moves("capablanca", CAPA, moves) + self.assertEqual(legals, result) + self.assertIn("f8i8", result) + + moves = ['a2a4', 'f7f5', 'b2b3', 'g8d5', 'b1a3', 'i8h6', 'c1a2', 'h8g6', 'c2c4'] + result = sf.legal_moves("capablanca", CAPA, moves) + self.assertIn("f8i8", result) + + moves = ['f2f4', 'g7g6', 'g1d4', 'j7j6', 'h1g3', 'b8a6', 'i1h3', 'h7h6'] + result = sf.legal_moves("capablanca", CAPA, moves) + self.assertIn("f1i1", result) + + def test_get_fen(self): + result = sf.get_fen("chess", CHESS, []) + self.assertEqual(result, CHESS) + + result = sf.get_fen("capablanca", CAPA, []) + self.assertEqual(result, CAPA) + + result = sf.get_fen("xiangqi", XIANGQI, []) + self.assertEqual(result, XIANGQI) + + fen = "rnab1kbcnr/ppppPppppp/10/4q5/10/10/PPPPP1PPPP/RNABQKBCNR[p] b KQkq - 0 3" + result = sf.get_fen("capahouse", CAPA, ["f2f4", "e7e5", "f4e5", "e8e5", "P@e7"]) + self.assertEqual(result, fen) + + fen0 = "reb1k2r/ppppqppp/2nb1n2/4p3/4P3/N1P2N2/PB1PQPPP/RE2KBHR[h] b KQkqac - 2 6" + fen1 = "reb2rk1/ppppqppp/2nb1n2/4p3/4P3/N1P2N2/PB1PQPPP/RE2KBHR[h] w KQac - 3 7" + result = sf.get_fen("seirawan", fen0, ["e8g8"]) + self.assertEqual(result, fen1) + + result = sf.get_fen("chess", CHESS, [], True, False, False) + self.assertEqual(result, CHESS960) + + # test O-O-O + fen = "rbkqnrbn/pppppppp/8/8/8/8/PPPPPPPP/RBKQNRBN w AFaf - 0 1" + moves = ["d2d4", "f7f5", "e1f3", "h8g6", "h1g3", "c7c6", "c2c3", "e7e6", "b1d3", "d7d5", "d1c2", "b8d6", "e2e3", "d8d7", "c1a1"] + result = sf.get_fen("chess", fen, moves, True, False, False) + self.assertEqual(result, "r1k1nrb1/pp1q2pp/2pbp1n1/3p1p2/3P4/2PBPNN1/PPQ2PPP/2KR1RB1 b fa - 2 8", CHESS960) + + # SFEN + result = sf.get_fen("shogi", SHOGI, [], False, True) + self.assertEqual(result, SHOGI_SFEN) + + # makruk FEN + fen = "rnsmksnr/8/1pM~1pppp/p7/8/PPPP1PPP/8/RNSKMSNR b - - 0 3" + result = sf.get_fen("makruk", MAKRUK, ["e3e4", "d6d5", "e4d5", "a6a5", "d5c6m"], False, False, True) + self.assertEqual(result, fen) + result = sf.get_fen("makruk", fen, [], False, False, True) + self.assertEqual(result, fen) + + def test_get_san(self): + fen = "4k3/8/3R4/8/1R3R2/8/3R4/4K3 w - - 0 1" + result = sf.get_san("chess", fen, "b4d4") + self.assertEqual(result, "Rbd4") + + result = sf.get_san("chess", fen, "f4d4") + self.assertEqual(result, "Rfd4") + + result = sf.get_san("chess", fen, "d2d4") + self.assertEqual(result, "R2d4") + + result = sf.get_san("chess", fen, "d6d4") + self.assertEqual(result, "R6d4") + + fen = "4k3/8/3R4/3P4/1RP1PR2/8/3R4/4K3 w - - 0 1" + result = sf.get_san("chess", fen, "d2d4") + self.assertEqual(result, "Rd4") + + fen = "1r2k3/P1P5/8/8/8/8/8/4K3 w - - 0 1" + result = sf.get_san("chess", fen, "c7b8q") + self.assertEqual(result, "cxb8=Q+") + + result = sf.get_san("capablanca", CAPA, "e2e4") + self.assertEqual(result, "e4") + + result = sf.get_san("capablanca", CAPA, "h1i3") + self.assertEqual(result, "Ci3") + + result = sf.get_san("sittuyin", SITTUYIN, "R@a1") + self.assertEqual(result, "R@a1") + + fen = "3rr3/1kn3n1/1ss1p1pp/1pPpP3/6PP/p3KN2/2SSFN2/3R3R[] b - - 0 14" + result = sf.get_san("sittuyin", fen, "c6c5") + self.assertEqual(result, "Scxc5") + + fen = "7R/1r6/3k1np1/3s2N1/3s3P/4n3/6p1/2R3K1[] w - - 2 55" + result = sf.get_san("sittuyin", fen, "h4h4f") + self.assertEqual(result, "h4=F") + + result = sf.get_san("shogi", SHOGI, "i3i4") + self.assertEqual(result, "P-1f") + + result = sf.get_san("shogi", SHOGI, "f1e2") + self.assertEqual(result, "G4i-5h") + + fen = "lnsgkgsnl/1r5b1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/1B5R1/LNSGKGSNL w -" + result = sf.get_san("shogi", fen, "b2h8") + self.assertEqual(result, "Bx2b=") + result = sf.get_san("shogi", fen, "b2h8+") + self.assertEqual(result, "Bx2b+") + + fen = "lnsgkg1nl/1r5s1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/7R1/LNSGKGSNL[Bb] w " + result = sf.get_san("shogi", fen, "B@g7") + self.assertEqual(result, "B*3c") + + fen = "lnsgkg1nl/1r4s+B1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/7R1/LNSGKGSNL[B] w " + result = sf.get_san("shogi", fen, "h8g7") + self.assertEqual(result, "+B-3c") + + fen = "lnk2gsnl/7b1/p1p+SGp1pp/6p2/1pP6/4P4/PP3PPPP/1S2G2R1/L2GK1bNL[PRppns] w " + result = sf.get_san("shogi", fen, "d7d8") + self.assertEqual(result, "+S-6b") + + result = sf.get_san("xiangqi", XIANGQI, "h1g3") + self.assertEqual(result, "Hg3") + + result = sf.get_san("xiangqi", XIANGQI, "c1e3") + self.assertEqual(result, "Ece3") + + result = sf.get_san("xiangqi", XIANGQI, "h3h10") + self.assertEqual(result, "Cxh10") + + result = sf.get_san("xiangqi", XIANGQI, "h3h5") + self.assertEqual(result, "Ch5") + + fen = "1rb1ka2r/4a4/2ncb1nc1/p1p1p1p1p/9/2P6/P3PNP1P/2N1C2C1/9/R1BAKAB1R w - - 1 7" + result = sf.get_san("xiangqi", fen, "c3e2") + self.assertEqual(result, "Hce2") + + result = sf.get_san("xiangqi", fen, "c3d5") + self.assertEqual(result, "Hd5") + + fen = "rnsm1s1r/4n1k1/1ppppppp/p7/2PPP3/PP3PPP/4N2R/RNSKMS2 b - - 1 5" + result = sf.get_san("makruk", fen, "f8f7") + self.assertEqual(result, "Sf7") + + fen = "4k3/8/8/4S3/8/2S5/8/4K3 w - - 0 1" + result = sf.get_san("makruk", fen, "e5d4") + self.assertEqual(result, "Sed4") + + result = sf.get_san("makruk", fen, "c3d4") + self.assertEqual(result, "Scd4") + + fen = "4k3/8/8/3S4/8/3S4/8/4K3 w - - 0 1" + result = sf.get_san("makruk", fen, "d3d4") + self.assertEqual(result, "Sd4") + + UCI_moves = ["e2e4", "e7e5", "g1f3", "b8c6h", "f1c4", "f8c5e"] + SAN_moves = ["e4", "e5", "Nf3", "Nc6/H", "Bc4", "Bc5/E"] + + fen = SEIRAWAN + for i, move in enumerate(UCI_moves): + result = sf.get_san("seirawan", fen, move) + self.assertEqual(result, SAN_moves[i]) + fen = sf.get_fen("seirawan", SEIRAWAN, UCI_moves[:i + 1]) + + result = sf.get_san("seirawan", fen, "e1g1") + self.assertEqual(result, "O-O") + + result = sf.get_san("seirawan", fen, "e1g1h") + self.assertEqual(result, "O-O/He1") + result = sf.get_san("seirawan", fen, "e1g1e") + self.assertEqual(result, "O-O/Ee1") + + result = sf.get_san("seirawan", fen, "h1e1h") + self.assertEqual(result, "O-O/Hh1") + result = sf.get_san("seirawan", fen, "h1e1e") + self.assertEqual(result, "O-O/Eh1") + + def test_get_san_moves(self): + UCI_moves = ["e2e4", "e7e5", "g1f3", "b8c6h", "f1c4", "f8c5e"] + SAN_moves = ["e4", "e5", "Nf3", "Nc6/H", "Bc4", "Bc5/E"] + + result = sf.get_san_moves("seirawan", SEIRAWAN, UCI_moves) + self.assertEqual(result, SAN_moves) + + def test_gives_check(self): + result = sf.gives_check("capablanca", CAPA, []) + self.assertFalse(result) + + result = sf.gives_check("capablanca", CAPA, ["e2e4"]) + self.assertFalse(result) + + moves = ["g2g3", "d7d5", "a2a3", "c8h3"] + result = sf.gives_check("capablanca", CAPA, moves) + self.assertTrue(result) + + def test_game_result(self): + result = sf.game_result("chess", CHESS, ["f2f3", "e7e5", "g2g4", "d8h4"]) + self.assertTrue(result < 0) + + def test_is_immediate_game_end(self): + result = sf.is_immediate_game_end("capablanca", CAPA, []) + self.assertFalse(result[0]) + + # bikjang (facing kings) + moves = "e2e3 e9f9 h3d3 e7f7 i1i3 h10i8 i3h3 c10e7 h3h8 i10i9 h8b8 i9g9 d3f3 f9e9 f3f10 e7c10 f10c10 b10c8 c10g10 g9f9 b8c8 a10b10 b3f3 f9h9 a1a2 h9f9 a2d2 b10b9 d2d10 e9d10 c8c10 d10d9 f3f9 i8g9 f9b9 a7a6 g10g7 f7f6 e4e5 c7d7 g1e4 i7i6 e4b6 d9d8 c10c8 d8d9 b9g9 d7d6 b6e8 i6h6 e5e6 f6e6 c1e4 a6b6 e4b6 d6d5 c4c5 d9d10 e3d3 h6i6 c5c6 d5c5" + result = sf.is_immediate_game_end("janggi", JANGGI, moves.split()) + self.assertFalse(result[0]) + + moves = "e2e3 e9f9 h3d3 e7f7 i1i3 h10i8 i3h3 c10e7 h3h8 i10i9 h8b8 i9g9 d3f3 f9e9 f3f10 e7c10 f10c10 b10c8 c10g10 g9f9 b8c8 a10b10 b3f3 f9h9 a1a2 h9f9 a2d2 b10b9 d2d10 e9d10 c8c10 d10d9 f3f9 i8g9 f9b9 a7a6 g10g7 f7f6 e4e5 c7d7 g1e4 i7i6 e4b6 d9d8 c10c8 d8d9 b9g9 d7d6 b6e8 i6h6 e5e6 f6e6 c1e4 a6b6 e4b6 d6d5 c4c5 d9d10 e3d3 h6i6 c5c6 d5c5 d3d3" + result = sf.is_immediate_game_end("janggi", JANGGI, moves.split()) + self.assertTrue(result[0]) + self.assertTrue(result[1] < 0) + + def test_is_optional_game_end(self): + result = sf.is_optional_game_end("capablanca", CAPA, []) + self.assertFalse(result[0]) + + def test_has_insufficient_material(self): + for variant, positions in variant_positions.items(): + for fen, expected_result in positions.items(): + result = sf.has_insufficient_material(variant, fen, []) + self.assertEqual(result, expected_result, "{}: {}".format(variant, fen)) + + +if __name__ == '__main__': + unittest.main(verbosity=2)