diff --git a/ChessAndroid/app/CMakeLists.txt b/ChessAndroid/app/CMakeLists.txt index e3e90b1..5530874 100644 --- a/ChessAndroid/app/CMakeLists.txt +++ b/ChessAndroid/app/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.10) set(LIB_NAME "chess") project(${LIB_NAME}) +set(CMAKE_CXX_EXTENSIONS OFF) + add_library( ${LIB_NAME} SHARED @@ -33,9 +35,7 @@ target_compile_options(${LIB_NAME} PUBLIC -Wpedantic -Wnull-dereference ) -set(FLAGS_DEBUG "-O1") set(FLAGS_RELEASE "-DNDEBUG" "-flto=thin" "-O3") -target_compile_options(${LIB_NAME} PUBLIC "$<$:${FLAGS_DEBUG}>") target_compile_options(${LIB_NAME} PUBLIC "$<$:${FLAGS_RELEASE}>") target_compile_options(${LIB_NAME} PUBLIC "$<$:${FLAGS_RELEASE}>") diff --git a/ChessAndroid/app/build.gradle b/ChessAndroid/app/build.gradle index 679e95b..bb7e4c9 100644 --- a/ChessAndroid/app/build.gradle +++ b/ChessAndroid/app/build.gradle @@ -9,8 +9,9 @@ android { applicationId "net.theluckycoder.chess" minSdkVersion 21 targetSdkVersion 30 - versionCode 1200 + versionCode 1204 versionName "1.2.0" + resConfigs "en" } buildTypes { @@ -19,12 +20,6 @@ android { shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - externalNativeBuild { - cmake { - abiFilters "arm64-v8a", "armeabi-v7a" - } - } - packagingOptions { resources { excludes.add("DebugProbesKt.bin") @@ -33,11 +28,6 @@ android { } } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - buildFeatures.compose = true composeOptions { @@ -68,18 +58,18 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - debugImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3") // AndroidX - implementation("androidx.activity:activity-ktx:1.2.2") + implementation("androidx.activity:activity-ktx:1.2.3") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") - implementation("androidx.datastore:datastore-preferences:1.0.0-alpha08") + implementation("androidx.datastore:datastore-preferences:1.0.0-beta01") // Compose implementation("androidx.compose.ui:ui:$compose_version") - implementation("androidx.compose.ui:ui-tooling:$compose_version") implementation("androidx.compose.foundation:foundation:$compose_version") implementation("androidx.compose.material:material:$compose_version") + implementation("androidx.compose.ui:ui-tooling:$compose_version") + debugImplementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha04") } diff --git a/ChessAndroid/app/src/main/AndroidManifest.xml b/ChessAndroid/app/src/main/AndroidManifest.xml index 61844ed..6de929a 100644 --- a/ChessAndroid/app/src/main/AndroidManifest.xml +++ b/ChessAndroid/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:label="@string/app_name" android:largeHeap="true" android:supportsRtl="true" + android:isGame="true" android:theme="@style/AppTheme.NoActionBar" tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> diff --git a/ChessAndroid/app/src/main/assets/Book.bin b/ChessAndroid/app/src/main/assets/OpeningBook.bin similarity index 100% rename from ChessAndroid/app/src/main/assets/Book.bin rename to ChessAndroid/app/src/main/assets/OpeningBook.bin diff --git a/ChessAndroid/app/src/main/cpp/JniCache.h b/ChessAndroid/app/src/main/cpp/JniCache.h deleted file mode 100644 index 00d6a6b..0000000 --- a/ChessAndroid/app/src/main/cpp/JniCache.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include - -// JVM Cached Classes -namespace JniCache -{ - jclass boardChangeListenerClass; - jclass indexedPieceClass; - jclass moveClass; - jclass searchOptionsClass; - - static jclass cacheClass(JNIEnv *env, const char *name) - { - return static_cast(env->NewGlobalRef(env->FindClass(name))); - } - - void createCaches(JNIEnv *env) - { - boardChangeListenerClass = cacheClass(env, "net/theluckycoder/chess/BoardChangeListener"); - indexedPieceClass = cacheClass(env, "net/theluckycoder/chess/model/IndexedPiece"); - moveClass = cacheClass(env, "net/theluckycoder/chess/model/Move"); - searchOptionsClass = cacheClass(env, "net/theluckycoder/chess/model/SearchOptions"); - } - - void cleanCaches(JNIEnv *env) - { - env->DeleteGlobalRef(boardChangeListenerClass); - env->DeleteGlobalRef(indexedPieceClass); - env->DeleteGlobalRef(moveClass); - env->DeleteGlobalRef(searchOptionsClass); - } -} diff --git a/ChessAndroid/app/src/main/cpp/JniUtils.h b/ChessAndroid/app/src/main/cpp/JniUtils.h new file mode 100644 index 0000000..4da73e3 --- /dev/null +++ b/ChessAndroid/app/src/main/cpp/JniUtils.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +#define LOGV(LOG_TAG, ...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) +#define LOGI(LOG_TAG, ...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGD(LOG_TAG, ...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#define LOGW(LOG_TAG, ...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(LOG_TAG, ...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +/** + * This class automatically attaches this thread to the JVM if necessary, + * and detaches it upon destruction. + * And it provides a valid pointer to the JNIEnv. + * + * In this way it works similarly to the std::lock_guard class. + */ +class NativeThreadAttach +{ +public: + explicit NativeThreadAttach(JavaVM *jvm) + : _jvm(jvm) + { + _jvmEnvState = _jvm->GetEnv(reinterpret_cast(&_env), JNI_VERSION_1_6); + + if (_jvmEnvState == JNI_EDETACHED) + _jvm->AttachCurrentThread(&_env, nullptr); + _env->ExceptionClear(); + } + + JNIEnv *getEnv() { return _env; } + + ~NativeThreadAttach() + { + if (_jvmEnvState == JNI_EDETACHED) + _jvm->DetachCurrentThread(); + } + +private: + JavaVM *_jvm; + JNIEnv *_env = nullptr; + int _jvmEnvState{}; +}; + +namespace JniCache +{ + jclass boardChangeListenerClass; + jclass searchListenerClass; + jclass indexedPieceClass; + jclass moveClass; + jclass searchOptionsClass; + + static jclass cacheClass(JNIEnv *env, const char *name) + { + return static_cast(env->NewGlobalRef(env->FindClass(name))); + } + + void createCaches(JNIEnv *env) + { + boardChangeListenerClass = cacheClass(env, "net/theluckycoder/chess/cpp/BoardChangeListener"); + searchListenerClass = cacheClass(env, "net/theluckycoder/chess/cpp/SearchListener"); + indexedPieceClass = cacheClass(env, "net/theluckycoder/chess/model/IndexedPiece"); + moveClass = cacheClass(env, "net/theluckycoder/chess/model/Move"); + searchOptionsClass = cacheClass(env, "net/theluckycoder/chess/model/SearchOptions"); + } + + void cleanCaches(JNIEnv *env) + { + env->DeleteGlobalRef(boardChangeListenerClass); + env->DeleteGlobalRef(searchListenerClass); + env->DeleteGlobalRef(indexedPieceClass); + env->DeleteGlobalRef(moveClass); + env->DeleteGlobalRef(searchOptionsClass); + } +} + diff --git a/ChessAndroid/app/src/main/cpp/Log.h b/ChessAndroid/app/src/main/cpp/Log.h deleted file mode 100644 index 905ad2c..0000000 --- a/ChessAndroid/app/src/main/cpp/Log.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -#define LOGV(LOG_TAG, ...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) -#define LOGI(LOG_TAG, ...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) -#define LOGD(LOG_TAG, ...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) -#define LOGW(LOG_TAG, ...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) -#define LOGE(LOG_TAG, ...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) diff --git a/ChessAndroid/app/src/main/cpp/Main.cpp b/ChessAndroid/app/src/main/cpp/Main.cpp index de9000d..8f79226 100644 --- a/ChessAndroid/app/src/main/cpp/Main.cpp +++ b/ChessAndroid/app/src/main/cpp/Main.cpp @@ -2,8 +2,7 @@ #include -#include "Log.h" -#include "JniCache.h" +#include "JniUtils.h" #include "AndroidBuffer.h" #include "chess/BoardManager.h" @@ -14,41 +13,40 @@ #include "chess/algorithm/Search.h" #include "chess/polyglot/PolyBook.h" -#define external extern "C" JNIEXPORT +#define ExportFunction extern "C" JNIEXPORT static constexpr auto TAG = "ChessCpp"; static JavaVM *jvm = nullptr; -static jobject listenerInstance = nullptr; +static jobject boardChangeListener = nullptr; +static jobject searchListener = nullptr; -const BoardManager::BoardChangedCallback boardChangedCallback = [](GameState state) +const BoardManager::BoardChangedCallback boardChangedCallback = [](const GameState state) { - JNIEnv *env; - int getEnvStat = jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); - if (getEnvStat == JNI_EDETACHED) - { - jvm->AttachCurrentThread(&env, nullptr); - LOGD(TAG, "Attached to Thread"); - } - - env->ExceptionClear(); - - const static auto callbackId = env->GetMethodID(JniCache::boardChangeListenerClass, "boardChanged", "(I)V"); - if (listenerInstance == nullptr) - { - LOGE(TAG, "No NativeListener Instance found"); - return; - } - - env->CallVoidMethod(listenerInstance, callbackId, static_cast(state)); + NativeThreadAttach threadAttach{ jvm }; + auto env = threadAttach.getEnv(); + + const static auto callbackId = + env->GetMethodID(JniCache::boardChangeListenerClass, "boardChanged", "(I)V"); + if (boardChangeListener == nullptr) + LOGE(TAG, "No BoardChangeListener Instance found"); + else + env->CallVoidMethod(boardChangeListener, callbackId, static_cast(state)); +}; - if (getEnvStat == JNI_EDETACHED) - { - jvm->DetachCurrentThread(); - LOGD(TAG, "Detached from Thread"); - } +const BoardManager::SearchFinishedCallback searchFinishedCallback = [](const bool success) +{ + NativeThreadAttach threadAttach{ jvm }; + auto env = threadAttach.getEnv(); + + const static auto callbackId = + env->GetMethodID(JniCache::searchListenerClass, "onFinish", "(Z)V"); + if (searchListener == nullptr) + LOGE(TAG, "No SearchListener Instance found"); + else + env->CallVoidMethod(searchListener, callbackId, success); }; -external jint JNI_OnLoad(JavaVM *vm, void *) +ExportFunction jint JNI_OnLoad(JavaVM *vm, void *) { LOGI(TAG, "JNI_OnLoad"); @@ -65,7 +63,7 @@ external jint JNI_OnLoad(JavaVM *vm, void *) return JNI_VERSION_1_6; } -external void JNI_OnUnload(JavaVM *vm, void *) +ExportFunction void JNI_OnUnload(JavaVM *vm, void *) { LOGI(TAG, "JNI_OnUnload"); @@ -73,8 +71,10 @@ external void JNI_OnUnload(JavaVM *vm, void *) vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); // Clean the caches - env->DeleteGlobalRef(listenerInstance); - listenerInstance = nullptr; + env->DeleteGlobalRef(boardChangeListener); + env->DeleteGlobalRef(searchListener); + boardChangeListener = nullptr; + searchListener = nullptr; JniCache::cleanCaches(env); delete std::cout.rdbuf(nullptr); @@ -82,18 +82,20 @@ external void JNI_OnUnload(JavaVM *vm, void *) jvm = nullptr; } -external void JNICALL -Java_net_theluckycoder_chess_Native_initBoard(JNIEnv *pEnv, jobject, jobject instance, jboolean isPlayerWhite) +// Native Class + +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_initBoard(JNIEnv *pEnv, jobject, jobject instance, jboolean isPlayerWhite) { pEnv->ExceptionClear(); - if (!pEnv->IsSameObject(listenerInstance, instance)) + if (!pEnv->IsSameObject(boardChangeListener, instance)) { LOGD(TAG, "initBoardNative"); - if (listenerInstance) - pEnv->DeleteGlobalRef(listenerInstance); + if (boardChangeListener) + pEnv->DeleteGlobalRef(boardChangeListener); - listenerInstance = pEnv->NewGlobalRef(instance); + boardChangeListener = pEnv->NewGlobalRef(instance); BoardManager::initBoardManager(boardChangedCallback); } @@ -101,28 +103,54 @@ Java_net_theluckycoder_chess_Native_initBoard(JNIEnv *pEnv, jobject, jobject ins BoardManager::initBoardManager(boardChangedCallback, isPlayerWhite); } -// Native Class +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_initBook(JNIEnv *pEnv, jobject, jstring bookPath) +{ + const char *nativeString = pEnv->GetStringUTFChars(bookPath, nullptr); + PolyBook::initBook(nativeString); + LOGV("Book", "Initialized"); + + pEnv->ReleaseStringUTFChars(bookPath, nativeString); +} + +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_enableBook(JNIEnv *, jobject, jboolean enable) +{ + PolyBook::enable(static_cast(enable)); +} + +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_setSearchListener(JNIEnv *pEnv, jobject, jobject instance) +{ + if (!pEnv->IsSameObject(searchListener, instance)) + { + if (boardChangeListener) + pEnv->DeleteGlobalRef(searchListener); + searchListener = pEnv->NewGlobalRef(instance); + } + BoardManager::setSearchFinishedCallback(searchFinishedCallback); +} -external jboolean JNICALL -Java_net_theluckycoder_chess_Native_isEngineWorking(JNIEnv *, jobject) +ExportFunction jboolean JNICALL +Java_net_theluckycoder_chess_cpp_Native_isEngineBusy(JNIEnv *, jobject) { - return static_cast(BoardManager::isWorking()); + return static_cast(BoardManager::isEngineBusy()); } -external jboolean JNICALL -Java_net_theluckycoder_chess_Native_isPlayerWhite(JNIEnv *, jobject) +ExportFunction jboolean JNICALL +Java_net_theluckycoder_chess_cpp_Native_isPlayerWhite(JNIEnv *, jobject) { return static_cast(BoardManager::isPlayerWhite()); } -external jboolean JNICALL -Java_net_theluckycoder_chess_Native_isPlayersTurn(JNIEnv *, jobject) +ExportFunction jboolean JNICALL +Java_net_theluckycoder_chess_cpp_Native_isPlayersTurn(JNIEnv *, jobject) { return static_cast(BoardManager::isPlayerWhite() == BoardManager::getBoard().colorToMove); } -external jobjectArray JNICALL -Java_net_theluckycoder_chess_Native_getPieces(JNIEnv *pEnv, jobject) +ExportFunction jobjectArray JNICALL +Java_net_theluckycoder_chess_cpp_Native_getPieces(JNIEnv *pEnv, jobject) { pEnv->ExceptionClear(); @@ -144,8 +172,8 @@ Java_net_theluckycoder_chess_Native_getPieces(JNIEnv *pEnv, jobject) return array; } -external jobjectArray JNICALL -Java_net_theluckycoder_chess_Native_getPossibleMoves(JNIEnv *pEnv, jobject, jbyte square) +ExportFunction jobjectArray JNICALL +Java_net_theluckycoder_chess_cpp_Native_getPossibleMoves(JNIEnv *pEnv, jobject, jbyte square) { const static auto constructorId = pEnv->GetMethodID(JniCache::moveClass, "", "(IBBBBBB)V"); @@ -166,68 +194,40 @@ Java_net_theluckycoder_chess_Native_getPossibleMoves(JNIEnv *pEnv, jobject, jbyt return result; } -external jobject JNICALL -Java_net_theluckycoder_chess_Native_getSearchOptions(JNIEnv *pEnv, jobject) -{ - const static auto constructorId = pEnv->GetMethodID(JniCache::searchOptionsClass, "", "(IIJIZ)V"); - - const auto options = BoardManager::getSearchOptions(); - - return pEnv->NewObject(JniCache::searchOptionsClass, constructorId, - options.depth(), options.threadCount(), - static_cast(options.searchTime()), options.tableSizeMb(), - options.quietSearch()); -} - -external void JNICALL -Java_net_theluckycoder_chess_Native_setSearchOptions(JNIEnv *, jobject, jint searchDepth, - jboolean quietSearch, - jint threadCount, - jint hashSizeMb, - jlong searchTime) -{ - BoardManager::setSearchOptions({ searchDepth, - static_cast(threadCount), - static_cast(hashSizeMb), - static_cast(quietSearch), - static_cast(searchTime) }); -} - - -external void JNICALL -Java_net_theluckycoder_chess_Native_makeMove(JNIEnv *, jobject, jint move) +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_makeMove(JNIEnv *, jobject, jint move) { BoardManager::makeMove(Move{ static_cast(move) }); } -external void JNICALL -Java_net_theluckycoder_chess_Native_makeEngineMove(JNIEnv *, jobject) +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_makeEngineMove(JNIEnv *, jobject) { BoardManager::makeEngineMove(); } -external void JNICALL -Java_net_theluckycoder_chess_Native_stopSearch(JNIEnv *, jobject) +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_stopSearch(JNIEnv *, jobject) { Search::stopSearch(); } -external void JNICALL -Java_net_theluckycoder_chess_Native_undoMoves(JNIEnv *, jobject) +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_undoMoves(JNIEnv *, jobject) { BoardManager::undoLastMoves(); } -external void JNICALL -Java_net_theluckycoder_chess_Native_redoMoves(JNIEnv *, jobject) +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_redoMoves(JNIEnv *, jobject) { BoardManager::redoLastMoves(); } -external jboolean JNICALL -Java_net_theluckycoder_chess_Native_loadFen(JNIEnv *pEnv, jobject, jboolean playerWhite, - jstring fenPosition) +ExportFunction jboolean JNICALL +Java_net_theluckycoder_chess_cpp_Native_loadFen(JNIEnv *pEnv, jobject, jboolean playerWhite, + jstring fenPosition) { const char *nativeString = pEnv->GetStringUTFChars(fenPosition, nullptr); @@ -238,9 +238,9 @@ Java_net_theluckycoder_chess_Native_loadFen(JNIEnv *pEnv, jobject, jboolean play return static_cast(loaded); } -external void JNICALL -Java_net_theluckycoder_chess_Native_loadFenMoves(JNIEnv *pEnv, jobject, - jboolean isPlayerWhite, jstring fenPosition, jintArray moves) +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Native_loadFenMoves(JNIEnv *pEnv, jobject, + jboolean isPlayerWhite, jstring fenPosition, jintArray moves) { const usize size = pEnv->GetArrayLength(moves); auto elements = pEnv->GetIntArrayElements(moves, JNI_FALSE); @@ -258,22 +258,22 @@ Java_net_theluckycoder_chess_Native_loadFenMoves(JNIEnv *pEnv, jobject, pEnv->ReleaseStringUTFChars(fenPosition, nativeString); } -external jstring JNICALL -Java_net_theluckycoder_chess_Native_getCurrentFen(JNIEnv *pEnv, jobject) +ExportFunction jstring JNICALL +Java_net_theluckycoder_chess_cpp_Native_getCurrentFen(JNIEnv *pEnv, jobject) { const auto fenString = BoardManager::getBoard().getFen(); return pEnv->NewStringUTF(fenString.c_str()); } -external jstring JNICALL -Java_net_theluckycoder_chess_Native_getStartFen(JNIEnv *pEnv, jobject) +ExportFunction jstring JNICALL +Java_net_theluckycoder_chess_cpp_Native_getStartFen(JNIEnv *pEnv, jobject) { const auto &fenString = BoardManager::getMovesStack().getStartFen(); return pEnv->NewStringUTF(fenString.c_str()); } -external jobjectArray JNICALL -Java_net_theluckycoder_chess_Native_getMovesHistory(JNIEnv *pEnv, jobject) +ExportFunction jobjectArray JNICALL +Java_net_theluckycoder_chess_cpp_Native_getMovesHistory(JNIEnv *pEnv, jobject) { const auto stack = BoardManager::getMovesStack(); const static auto constructorId = pEnv->GetMethodID(JniCache::moveClass, "", "(IBBBBBB)V"); @@ -294,30 +294,22 @@ Java_net_theluckycoder_chess_Native_getMovesHistory(JNIEnv *pEnv, jobject) return result; } -external jint JNICALL -Java_net_theluckycoder_chess_Native_getCurrentMoveIndex(JNIEnv *, jobject) +ExportFunction jint JNICALL +Java_net_theluckycoder_chess_cpp_Native_getCurrentMoveIndex(JNIEnv *, jobject) { return BoardManager::getMovesStack().getCurrentIndex(); } -external void JNICALL -Java_net_theluckycoder_chess_Native_initBook(JNIEnv *pEnv, jobject, jstring bookPath) -{ - const char *nativeString = pEnv->GetStringUTFChars(bookPath, nullptr); - PolyBook::initBook(nativeString); - LOGV("Book", "Initialized"); +// region Tests - pEnv->ReleaseStringUTFChars(bookPath, nativeString); -} - -external void JNICALL -Java_net_theluckycoder_chess_Native_perftTests(JNIEnv *, jobject) +ExportFunction void JNICALL +Java_net_theluckycoder_chess_cpp_Tests_perftTests(JNIEnv *, jobject) { Tests::runPerftTests(); } -external jstring JNICALL -Java_net_theluckycoder_chess_Native_evaluationTests(JNIEnv *pEnv, jobject) +ExportFunction jstring JNICALL +Java_net_theluckycoder_chess_cpp_Tests_evaluationTests(JNIEnv *pEnv, jobject) { const auto testResults = Tests::runEvaluationTests(); if (testResults.empty()) @@ -326,27 +318,60 @@ Java_net_theluckycoder_chess_Native_evaluationTests(JNIEnv *pEnv, jobject) return pEnv->NewStringUTF(testResults.c_str()); } +// endregion Tests + +// region SearchOptions + +ExportFunction jobject JNICALL +Java_net_theluckycoder_chess_model_SearchOptions_getNativeSearchOptions(JNIEnv *pEnv, jclass) +{ + const static auto constructorId = pEnv->GetMethodID(JniCache::searchOptionsClass, "", "(IIJIZ)V"); + + const auto options = BoardManager::getSearchOptions(); + + return pEnv->NewObject(JniCache::searchOptionsClass, constructorId, + options.depth(), options.threadCount(), + static_cast(options.searchTime()), options.tableSizeMb(), + options.quietSearch()); +} + +ExportFunction void JNICALL +Java_net_theluckycoder_chess_model_SearchOptions_setNativeSearchOptions(JNIEnv *, jclass, jint searchDepth, + jboolean quietSearch, + jint threadCount, + jint hashSizeMb, + jlong searchTime) +{ + BoardManager::setSearchOptions({ searchDepth, + static_cast(threadCount), + static_cast(hashSizeMb), + static_cast(quietSearch), + static_cast(searchTime) }); +} + +// endregion SearchOptions + // region DebugStats -external void JNICALL +ExportFunction void JNICALL Java_net_theluckycoder_chess_model_DebugStats_enable(JNIEnv *, jclass, jboolean enabled) { Stats::setEnabled(enabled); } -external jlong JNICALL +ExportFunction jlong JNICALL Java_net_theluckycoder_chess_model_DebugStats_getNativeSearchTime(JNIEnv *, jclass) { return static_cast(Stats::getElapsedMs()); } -external jint JNICALL +ExportFunction jint JNICALL Java_net_theluckycoder_chess_model_DebugStats_getNativeBoardEvaluation(JNIEnv *, jclass) { return static_cast(Evaluation::value(BoardManager::getBoard())); } -external jstring JNICALL +ExportFunction jstring JNICALL Java_net_theluckycoder_chess_model_DebugStats_getNativeAdvancedStats(JNIEnv *pEnv, jclass) { const auto stats = Stats::formatStats('\n'); diff --git a/ChessAndroid/app/src/main/cpp/chess/Bitfield.h b/ChessAndroid/app/src/main/cpp/chess/Bitfield.h new file mode 100644 index 0000000..db4d46b --- /dev/null +++ b/ChessAndroid/app/src/main/cpp/chess/Bitfield.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +namespace bitfield_detail +{ + + template + class field + { + public: + static constexpr std::size_t Mask = (1u << Size) - 1u; + + inline static constexpr T get(const T &t) + { + auto offset = Offset; + return (t >> offset) & Mask; + } + + inline static constexpr void set(T &t, const T &value) + { + constexpr auto ShiftedMask = Mask << Offset; + t = (t & ~ShiftedMask) | ((value & Mask) << Offset); + } + }; + + template + consteval std::size_t field_size() { return Size0; } + + template + consteval std::size_t field_size() + { + return Index ? field_size() : Size0; + } + + template + consteval std::size_t field_offset() + { + if constexpr (Index < MaxIndex) + return Size0; + return {}; + } + + template + consteval std::size_t field_offset() + { + if constexpr (Index < MaxIndex) + return Size0 + field_offset(); + return {}; + } +} + +template +class Bitfield +{ + + template + using bit_field_type = bitfield_detail::field(), bitfield_detail::field_offset<0, Index, Sizes...>()>; + +public: + constexpr Bitfield() = default; + + explicit constexpr Bitfield(const T t) : _t(t) {} + + template + constexpr T get() const { return bit_field_type::get(_t); } + + template + constexpr A getAs() const { return static_cast(get()); } + + template + constexpr void set(const T &value) { bit_field_type::set(_t, value); } + + template + constexpr void setAs(const A &value) { set(static_cast(value)); } + + constexpr T value() const { return _t; } + + constexpr T &ref() { return _t; } + +private: + T _t{}; +}; diff --git a/ChessAndroid/app/src/main/cpp/chess/Board.cpp b/ChessAndroid/app/src/main/cpp/chess/Board.cpp index 1e0d60d..66c83ea 100644 --- a/ChessAndroid/app/src/main/cpp/chess/Board.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/Board.cpp @@ -1,5 +1,6 @@ #include "Board.h" +#include "Psqt.h" #include "Zobrist.h" #include "algorithm/Evaluation.h" #include "persistence/FenParser.h" @@ -404,7 +405,8 @@ void Board::addPiece(const Square square, const Piece piece) noexcept if (piece.type() != PAWN) npm += Evaluation::getPieceValue(piece.type()); - pieceList[piece][pieceCount[piece]++] = square; + ++pieceCount[piece]; + psq += PSQT[piece.type()][square]; } void Board::movePiece(const Square from, const Square to) noexcept @@ -424,17 +426,8 @@ void Board::movePiece(const Square from, const Square to) noexcept getPiece(to) = piece; getPieces(piece).addSquare(to); - [[maybe_unused]] bool pieceMoved{}; - for (u8 &sq : pieceList[piece]) - { - if (sq == from) - { - sq = to; - pieceMoved = true; - break; - } - } - assert(pieceMoved); + psq -= PSQT[piece.type()][from]; + psq += PSQT[piece.type()][to]; } void Board::removePiece(const Square square) noexcept @@ -451,22 +444,8 @@ void Board::removePiece(const Square square) noexcept if (piece.type() != PAWN) npm -= Evaluation::getPieceValue(piece.type()); - auto &piecesSquares = pieceList[piece]; - - u8 pieceIndex = -1; - for (u8 i = 0; i < pieceCount[piece]; ++i) - { - if (piecesSquares[i] == square) - { - pieceIndex = i; - break; - } - } - - assert(pieceIndex < SQUARE_NB); - - // Move the last square in this unused index - piecesSquares[pieceIndex] = piecesSquares[--pieceCount[piece]]; + --pieceCount[piece]; + psq -= PSQT[piece.type()][square]; } Bitboard Board::findBlockers(const Bitboard sliders, const Square sq, Bitboard &pinners) const noexcept @@ -520,12 +499,13 @@ void Board::updatePieceList() noexcept const Piece piece = getPiece(sq); if (piece) { - pieceList[piece][pieceCount[piece]++] = sq; - getPieces(piece).addSquare(sq); if (piece.type() != PAWN) npm += Evaluation::getPieceValue(piece.type()); + + ++pieceCount[piece]; + psq += PSQT[piece.type()][sq]; } } } diff --git a/ChessAndroid/app/src/main/cpp/chess/Board.h b/ChessAndroid/app/src/main/cpp/chess/Board.h index 04817a5..2e19ce0 100644 --- a/ChessAndroid/app/src/main/cpp/chess/Board.h +++ b/ChessAndroid/app/src/main/cpp/chess/Board.h @@ -19,7 +19,7 @@ class BoardState final u32 moveContents{}; u8 castlingRights{}; - Square enPassantSq{}; + Square enPassantSq = SQ_NONE; u8 fiftyMoveRule{}; Move getMove() const noexcept { return Move{ moveContents }; } @@ -108,15 +108,15 @@ class Board final public: Bitboard occupied{}; std::array, COLOR_NB> pieces{}; - std::array, 15> pieceList{}; std::array pieceCount{}; std::array data{}; BoardState state{}; - short ply{}; - short npm{}; + i16 ply{}; + i16 npm{}; + Score psq{}; Color colorToMove{}; private: @@ -190,7 +190,7 @@ inline Bitboard Board::getPieces(const Color color) const noexcept inline Square Board::getKingSq(const Color color) const noexcept { - return toSquare(pieceList[Piece{ KING, color }][0]); + return getPieces(KING, color).bitScanForward(); } inline u64 Board::zKey() const noexcept diff --git a/ChessAndroid/app/src/main/cpp/chess/BoardManager.cpp b/ChessAndroid/app/src/main/cpp/chess/BoardManager.cpp index ed9f56d..6fbdabb 100644 --- a/ChessAndroid/app/src/main/cpp/chess/BoardManager.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/BoardManager.cpp @@ -6,9 +6,6 @@ #include "MoveGen.h" #include "algorithm/Search.h" -std::recursive_mutex BoardManager::_mutex; -BoardManager::BoardChangedCallback BoardManager::_callback; - void BoardManager::initBoardManager(const BoardChangedCallback &callback, const bool isPlayerWhite) { std::lock_guard lock{ _mutex }; @@ -18,9 +15,9 @@ void BoardManager::initBoardManager(const BoardChangedCallback &callback, const _isPlayerWhite = isPlayerWhite; _currentBoard.setToStartPos(); _movesStack = { _currentBoard }; - _callback = callback; + _boardCallback = callback; - _callback(GameState::NONE); + _boardCallback(GameState::NONE); } bool BoardManager::loadGame(const bool isPlayerWhite, const std::string &fen) @@ -33,7 +30,7 @@ bool BoardManager::loadGame(const bool isPlayerWhite, const std::string &fen) _isPlayerWhite = isPlayerWhite; _movesStack = { _currentBoard }; - _callback(getBoardState()); + _boardCallback(getBoardState()); return true; } @@ -57,7 +54,7 @@ bool BoardManager::loadGame(const bool isPlayerWhite, const std::string &fen, co _movesStack.push(_currentBoard, move); } - _callback(getBoardState()); + _boardCallback(getBoardState()); return true; } @@ -65,9 +62,10 @@ bool BoardManager::loadGame(const bool isPlayerWhite, const std::string &fen, co void BoardManager::makeMove(const Move move) { std::lock_guard lock{ _mutex }; + _currentBoard.makeMove(move); _movesStack.push(_currentBoard, move); - _callback(getBoardState()); + _boardCallback(getBoardState()); } void BoardManager::makeEngineMove() @@ -77,8 +75,8 @@ void BoardManager::makeEngineMove() !(state == GameState::NONE || state == GameState::WHITE_IN_CHECK || state == GameState::BLACK_IN_CHECK)) return; - if (_isWorking) return; - _isWorking = true; + if (_isBusy) return; + _isBusy = true; const auto startZKey = _currentBoard.zKey(); @@ -91,35 +89,44 @@ void BoardManager::makeEngineMove() // This will should start executing exactly after makeEngineMove() // if the key is not as the specified one, cancel the search - if (startZKey != _currentBoard.zKey()) return; + if (startZKey != _currentBoard.zKey()) + { + _searchCallback(false); + return; + } lock.unlock(); const Move bestMove = Search::findBestMove(tempBoard, options); - _isWorking = false; + _isBusy = false; lock.lock(); // Make sure the board has not changed in the time we were searching + if (_searchCallback) + _searchCallback(startZKey == _currentBoard.zKey()); + if (startZKey == _currentBoard.zKey()) makeMove(bestMove); else - std::cout << "Board was changed while searching, cannot make found move\n"; + std::cout << "Board was changed while searching, cannot make found move\n" << std::flush; }).detach(); } void BoardManager::undoLastMoves() { std::lock_guard lock{ _mutex }; + if (!_movesStack.undo().empty()) _currentBoard.undoMove(); if (_movesStack.peek().colorToMove() != isPlayerWhite() && !_movesStack.undo().empty()) _currentBoard.undoMove(); - _callback(getBoardState()); + _boardCallback(getBoardState()); } void BoardManager::redoLastMoves() { std::lock_guard lock{ _mutex }; + if (const Move move = _movesStack.redo(); !move.empty()) _currentBoard.makeMove(move); if (_movesStack.peek().colorToMove() != isPlayerWhite()) @@ -131,7 +138,7 @@ void BoardManager::redoLastMoves() makeEngineMove(); // If there was no move to redo, try to make one } - _callback(getBoardState()); + _boardCallback(getBoardState()); } std::vector BoardManager::getPossibleMoves(const Square from) diff --git a/ChessAndroid/app/src/main/cpp/chess/BoardManager.h b/ChessAndroid/app/src/main/cpp/chess/BoardManager.h index 334b47a..9ab1be3 100644 --- a/ChessAndroid/app/src/main/cpp/chess/BoardManager.h +++ b/ChessAndroid/app/src/main/cpp/chess/BoardManager.h @@ -29,12 +29,14 @@ class BoardManager final { public: using BoardChangedCallback = std::function; + using SearchFinishedCallback = std::function; private: - static std::recursive_mutex _mutex; - static BoardChangedCallback _callback; + inline static std::recursive_mutex _mutex; + inline static BoardChangedCallback _boardCallback; + inline static SearchFinishedCallback _searchCallback; - inline static std::atomic_bool _isWorking{ false }; + inline static std::atomic_bool _isBusy{ false }; inline static constinit bool _isPlayerWhite{ true }; inline static constinit Board _currentBoard{}; inline static UndoRedo::MovesStack _movesStack; @@ -52,7 +54,13 @@ class BoardManager final static void redoLastMoves(); /// Getters and Setters - static bool isWorking() noexcept { return _isWorking; } + static void setSearchFinishedCallback(const SearchFinishedCallback &callback) + { + std::lock_guard lock{ _mutex }; + _searchCallback = callback; + } + + static bool isEngineBusy() noexcept { return _isBusy; } static bool isPlayerWhite() noexcept { diff --git a/ChessAndroid/app/src/main/cpp/chess/Defs.h b/ChessAndroid/app/src/main/cpp/chess/Defs.h index e443ffb..fb73296 100644 --- a/ChessAndroid/app/src/main/cpp/chess/Defs.h +++ b/ChessAndroid/app/src/main/cpp/chess/Defs.h @@ -179,3 +179,70 @@ constexpr u8 distanceToRankEdge(const Square square) noexcept const auto rank = rankOf(square); return std::min(rank, 7u - rank); } + + +class Score final +{ +public: + Score() = default; + + constexpr Score(const i16 mg, const i16 eg) noexcept + : mg(mg), eg(eg) {} + + constexpr Score &operator=(const i16 rhs) noexcept + { + mg = rhs; + eg = rhs; + return *this; + } + + constexpr void operator+=(const Score &rhs) noexcept + { + mg += rhs.mg; + eg += rhs.eg; + } + + constexpr void operator-=(const Score &rhs) noexcept + { + mg -= rhs.mg; + eg -= rhs.eg; + } + + constexpr void operator+=(const i16 rhs) noexcept + { + mg += rhs; + eg += rhs; + } + + constexpr void operator-=(const i16 rhs) noexcept + { + mg -= rhs; + eg -= rhs; + } + + constexpr Score operator+(const Score &rhs) const noexcept + { + Score lhs = *this; + lhs.mg += rhs.mg; + lhs.eg += rhs.eg; + return lhs; + } + + constexpr Score operator-(const Score &rhs) const noexcept + { + Score lhs = *this; + lhs.mg -= rhs.mg; + lhs.eg -= rhs.eg; + return lhs; + } + + constexpr Score operator*(const i16 rhs) const noexcept + { + Score lhs = *this; + lhs.mg *= rhs; + lhs.eg *= rhs; + return lhs; + } + + i16 mg{}, eg{}; +}; diff --git a/ChessAndroid/app/src/main/cpp/chess/Move.h b/ChessAndroid/app/src/main/cpp/chess/Move.h index f62c63e..476ea50 100644 --- a/ChessAndroid/app/src/main/cpp/chess/Move.h +++ b/ChessAndroid/app/src/main/cpp/chess/Move.h @@ -1,219 +1,208 @@ -#pragma once - -#include - -#include "Defs.h" - -class Move final -{ -public: - class Flags final - { - public: - enum Internal - { - CAPTURE = 1, // The move is a capture - PROMOTION = 1 << 1, // The move is a promotion - KSIDE_CASTLE = 1 << 2, // The move is a king SideKey castle - QSIDE_CASTLE = 1 << 3, // The move is a queen SideKey castle - DOUBLE_PAWN_PUSH = 1 << 4, // The move is a double pawn push - EN_PASSANT = 1 << 5, // The move is an en passant capture (Do not set the CAPTURE flag too) - PV_MOVE = 1 << 6, // The move is a PV Move - }; - - public: - explicit constexpr Flags(const u8 flags) noexcept: _flags(flags & 0x7F) {} - - constexpr u8 getContents() const noexcept { return _flags; } - - constexpr bool capture() const noexcept { return _flags & Internal::CAPTURE; } - - constexpr bool promotion() const noexcept { return _flags & Internal::PROMOTION; } - - constexpr bool kSideCastle() const noexcept { return _flags & Internal::KSIDE_CASTLE; } - - constexpr bool qSideCastle() const noexcept { return _flags & Internal::QSIDE_CASTLE; } - - constexpr bool doublePawnPush() const noexcept { return _flags & Internal::DOUBLE_PAWN_PUSH; } - - constexpr bool enPassant() const noexcept { return _flags & Internal::EN_PASSANT; } - - constexpr bool pvMove() const noexcept { return _flags & Internal::PV_MOVE; } - - private: - u8 _flags; - }; - - Move() = default; - - explicit constexpr Move(const unsigned int move, const int score = 0) noexcept - : _move(move), _score(score) - { - } - - constexpr Move(const u8 from, const u8 to, const PieceType piece, const u8 flags = {}) noexcept - : _move(((from & FROM_MASK) << FROM_SHIFT) - | ((to & TO_MASK) << TO_SHIFT) - | ((piece & MOVED_MASK) << MOVED_SHIFT) - | ((flags & FLAGS_MASK) << FLAGS_SHIFT)) - { - } - - constexpr bool empty() const noexcept { return !static_cast(_move); } - - constexpr u32 getContents() const noexcept { return _move; } - - constexpr i32 getScore() const noexcept { return _score; } - - constexpr void setScore(const int score) noexcept - { - _score = score; - } - - constexpr PieceType piece() const noexcept - { - return static_cast((_move >> MOVED_SHIFT) & MOVED_MASK); - } - - constexpr PieceType capturedPiece() const noexcept - { - return static_cast((_move >> CAPTURED_SHIFT) & CAPTURED_MASK); - } - - constexpr void setCapturedPiece(const PieceType type) noexcept - { - constexpr u32 ShiftedMask = CAPTURED_MASK << CAPTURED_SHIFT; - _move = (_move & ~ShiftedMask) | ((type << CAPTURED_SHIFT) & ShiftedMask); - } - - constexpr PieceType promotedPiece() const noexcept - { - return static_cast((_move >> PROMOTED_SHIFT) & PROMOTED_MASK); - } - - constexpr void setPromotedPiece(const PieceType type) noexcept - { - constexpr u32 ShiftedMask = PROMOTED_MASK << PROMOTED_SHIFT; - _move = (_move & ~ShiftedMask) | ((type << PROMOTED_SHIFT) & ShiftedMask); - } - - constexpr Square from() const noexcept - { - return toSquare((_move >> FROM_SHIFT) & FROM_MASK); - } - - constexpr Square to() const noexcept - { - return toSquare((_move >> TO_SHIFT) & TO_MASK); - } - - constexpr Flags flags() const noexcept - { - return Flags(u8(_move >> FLAGS_SHIFT) & FLAGS_MASK); - } - - constexpr void setFlags(const u8 flags) noexcept - { - constexpr u32 ShiftedMask = FLAGS_MASK << FLAGS_SHIFT; - _move = (_move & ~ShiftedMask) | ((flags << FLAGS_SHIFT) & ShiftedMask); - } - - constexpr bool isTactical() const noexcept - { - const auto f = flags(); - return f.capture() | f.promotion(); - } - - constexpr u16 getFromToBits() const noexcept - { - constexpr u16 Mask = (FROM_MASK | (TO_MASK << TO_SHIFT)); - return _move & Mask; - } - - constexpr bool operator==(const Move &rhs) const noexcept - { - return _move == rhs._move; - } - - constexpr bool operator!=(const Move &rhs) const noexcept - { - return _move != rhs._move; - } - - std::string toString(const bool showPiece = false) const - { - std::string str; - str.reserve(5); - - const Square fromSq = from(); - const Square toSq = to(); - - if (showPiece) - { - const PieceType p = piece(); - char pChar = 'K'; - if (p == PAWN) - pChar = 'P'; - else if (p == KNIGHT) - pChar = 'N'; - else if (p == BISHOP) - pChar = 'B'; - else if (p == ROOK) - pChar = 'R'; - else if (p == QUEEN) - pChar = 'Q'; - - str += pChar; - } - - str += 'a' + i8(fileOf(fromSq)); - str += '1' + i8(rankOf(fromSq)); - - str += 'a' + i8(fileOf(toSq)); - str += '1' + i8(rankOf(toSq)); - - if (flags().promotion()) - { - const PieceType promoted = promotedPiece(); - if (showPiece) - str.erase(0, 1); - char p = 'Q'; - if (promoted == ROOK) - p = 'R'; - else if (promoted == BISHOP) - p = 'B'; - else if (promoted == KNIGHT) - p = 'N'; - - str += p; - } - - return str; - } - -private: - /* - * Bits 0 to 5 (6 bits) store the 'from' square - * Bits 6 to 11 (6 bits) store the 'to' square - * Bits 12 to 14 (3 bits) store the 'moved' piece type - * Bits 15 to 17 (3 bits) store the 'captured' piece type - * Bits 18 to 20 (3 bits) store the 'promoted' piece type - * Bits 21 to 27 (7 bits) store the 'flags' - */ - u32 _move{}; - i32 _score{}; - - static constexpr u32 FROM_SHIFT = 0; - static constexpr u32 TO_SHIFT = 6u; - static constexpr u32 MOVED_SHIFT = 12u; - static constexpr u32 CAPTURED_SHIFT = 16u; - static constexpr u32 PROMOTED_SHIFT = 20u; - static constexpr u32 FLAGS_SHIFT = 23u; - - static constexpr u32 FROM_MASK = 0b11'1111; - static constexpr u32 TO_MASK = 0b11'1111; - static constexpr u32 MOVED_MASK = 0b111; - static constexpr u32 CAPTURED_MASK = 0b111; - static constexpr u32 PROMOTED_MASK = 0b111; - static constexpr u32 FLAGS_MASK = 0b1111'1111; -}; +#pragma once + +#include + +#include "Defs.h" +#include "Bitfield.h" + +class Move final +{ +public: + class Flags final + { + public: + enum Internal + { + CAPTURE = 1, // The move is a capture + PROMOTION = 1 << 1, // The move is a promotion + KSIDE_CASTLE = 1 << 2, // The move is a king side castle + QSIDE_CASTLE = 1 << 3, // The move is a queen side castle + DOUBLE_PAWN_PUSH = 1 << 4, // The move is a double pawn push + EN_PASSANT = 1 << 5, // The move is an en passant capture (Do not set the CAPTURE flag too) + PV_MOVE = 1 << 6, // The move is a PV Move + }; + + public: + explicit constexpr Flags(const u8 flags) noexcept: _flags(flags) {} + + constexpr u8 getContents() const noexcept { return _flags; } + + constexpr bool capture() const noexcept { return _flags & Internal::CAPTURE; } + + constexpr bool promotion() const noexcept { return _flags & Internal::PROMOTION; } + + constexpr bool kSideCastle() const noexcept { return _flags & Internal::KSIDE_CASTLE; } + + constexpr bool qSideCastle() const noexcept { return _flags & Internal::QSIDE_CASTLE; } + + constexpr bool doublePawnPush() const noexcept { return _flags & Internal::DOUBLE_PAWN_PUSH; } + + constexpr bool enPassant() const noexcept { return _flags & Internal::EN_PASSANT; } + + constexpr bool pvMove() const noexcept { return _flags & Internal::PV_MOVE; } + + private: + u8 _flags; + }; + + constexpr Move() = default; + + explicit constexpr Move(const u32 move, const i32 score = 0) noexcept + : _move(move), _score(score) + { + } + + constexpr Move(const u8 from, const u8 to, const PieceType piece) noexcept + { + _move.set<0>(from); + _move.set<1>(to); + _move.setAs<2>(piece); + } + + constexpr Move(const u8 from, const u8 to, const PieceType piece, const u8 flags) noexcept + : Move(from, to, piece) + { + setFlags(flags); + } + + constexpr bool empty() const noexcept { return !static_cast(_move.value()); } + + constexpr u32 getContents() const noexcept { return _move.value(); } + + constexpr i32 getScore() const noexcept { return _score; } + + constexpr void setScore(const int score) noexcept + { + _score = score; + } + + constexpr Square from() const noexcept + { + return toSquare(_move.get<0>()); + } + + constexpr Square to() const noexcept + { + return toSquare(_move.get<1>()); + } + + constexpr PieceType piece() const noexcept + { + return _move.getAs<2, PieceType>(); + } + + constexpr PieceType capturedPiece() const noexcept + { + return _move.getAs<3, PieceType>(); + } + + constexpr void setCapturedPiece(const PieceType type) noexcept + { + _move.setAs<3>(type); + } + + constexpr PieceType promotedPiece() const noexcept + { + return _move.getAs<4, PieceType>(); + } + + constexpr void setPromotedPiece(const PieceType type) noexcept + { + _move.setAs<4>(type); + } + + constexpr Flags flags() const noexcept + { + return _move.getAs<5, Flags>(); + } + + constexpr void setFlags(const u8 flags) noexcept + { + _move.set<5>(flags); + } + + constexpr bool isTactical() const noexcept + { + const auto f = flags(); + return f.capture() | f.promotion(); + } + + constexpr u16 getFromToBits() const noexcept + { + constexpr u16 Mask = (0x3Fu | (0x3Fu << 6u)); + return _move.value() & Mask; + } + + constexpr bool operator==(const Move &rhs) const noexcept + { + return _move.value() == rhs._move.value(); + } + + constexpr bool operator!=(const Move &rhs) const noexcept + { + return _move.value() != rhs._move.value(); + } + + std::string toString(const bool showPiece = false) const + { + std::string str; + str.reserve(5); + + const Square fromSq = from(); + const Square toSq = to(); + + if (showPiece) + { + const PieceType p = piece(); + char pChar = 'K'; + if (p == PAWN) + pChar = 'P'; + else if (p == KNIGHT) + pChar = 'N'; + else if (p == BISHOP) + pChar = 'B'; + else if (p == ROOK) + pChar = 'R'; + else if (p == QUEEN) + pChar = 'Q'; + + str += pChar; + } + + str += 'a' + i8(fileOf(fromSq)); + str += '1' + i8(rankOf(fromSq)); + + str += 'a' + i8(fileOf(toSq)); + str += '1' + i8(rankOf(toSq)); + + if (flags().promotion()) + { + const PieceType promoted = promotedPiece(); + if (showPiece) + str.erase(0, 1); + char p = 'Q'; + if (promoted == ROOK) + p = 'R'; + else if (promoted == BISHOP) + p = 'B'; + else if (promoted == KNIGHT) + p = 'N'; + + str += p; + } + + return str; + } + +private: + /* + * Bits 0 to 5 (6 bits) - 'from' square + * Bits 6 to 11 (6 bits) - 'to' square + * Bits 12 to 14 (3 bits) - 'moved' piece type + * Bits 15 to 17 (3 bits) - 'captured' piece type + * Bits 18 to 20 (3 bits) - 'promoted' piece type + * Bits 21 to 27 (7 bits) - 'flags' + */ + Bitfield _move{}; + i32 _score{}; +}; diff --git a/ChessAndroid/app/src/main/cpp/chess/PawnStructureTable.h b/ChessAndroid/app/src/main/cpp/chess/PawnStructureTable.h index 2aa5a18..322b432 100644 --- a/ChessAndroid/app/src/main/cpp/chess/PawnStructureTable.h +++ b/ChessAndroid/app/src/main/cpp/chess/PawnStructureTable.h @@ -1,7 +1,6 @@ #pragma once #include "Bitboard.h" -#include "Score.h" struct PawnStructureEntry { diff --git a/ChessAndroid/app/src/main/cpp/chess/Psqt.h b/ChessAndroid/app/src/main/cpp/chess/Psqt.h index e7675aa..685ae34 100644 --- a/ChessAndroid/app/src/main/cpp/chess/Psqt.h +++ b/ChessAndroid/app/src/main/cpp/chess/Psqt.h @@ -3,7 +3,6 @@ #include #include "Defs.h" -#include "Score.h" constexpr auto PSQT = [] { diff --git a/ChessAndroid/app/src/main/cpp/chess/Score.h b/ChessAndroid/app/src/main/cpp/chess/Score.h deleted file mode 100644 index d7beba0..0000000 --- a/ChessAndroid/app/src/main/cpp/chess/Score.h +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -class Score final -{ -public: - Score() = default; - - constexpr Score(const i16 mg, const i16 eg) noexcept - : mg(mg), eg(eg) {} - - constexpr Score &operator=(const i16 rhs) noexcept - { - mg = rhs; - eg = rhs; - return *this; - } - - constexpr void operator+=(const Score &rhs) noexcept - { - mg += rhs.mg; - eg += rhs.eg; - } - - constexpr void operator-=(const Score &rhs) noexcept - { - mg -= rhs.mg; - eg -= rhs.eg; - } - - constexpr void operator+=(const i16 rhs) noexcept - { - mg += rhs; - eg += rhs; - } - - constexpr void operator-=(const i16 rhs) noexcept - { - mg -= rhs; - eg -= rhs; - } - - constexpr Score operator+(const Score &rhs) const noexcept - { - Score lhs = *this; - lhs.mg += rhs.mg; - lhs.eg += rhs.eg; - return lhs; - } - - constexpr Score operator-(const Score &rhs) const noexcept - { - Score lhs = *this; - lhs.mg -= rhs.mg; - lhs.eg -= rhs.eg; - return lhs; - } - - constexpr Score operator*(const i16 rhs) const noexcept - { - Score lhs = *this; - lhs.mg *= rhs; - lhs.eg *= rhs; - return lhs; - } - - i16 mg{}, eg{}; -}; diff --git a/ChessAndroid/app/src/main/cpp/chess/SearchOptions.h b/ChessAndroid/app/src/main/cpp/chess/SearchOptions.h index 5eddaba..23b67e4 100644 --- a/ChessAndroid/app/src/main/cpp/chess/SearchOptions.h +++ b/ChessAndroid/app/src/main/cpp/chess/SearchOptions.h @@ -10,13 +10,13 @@ class SearchOptions final public: SearchOptions() : SearchOptions(6, std::max(1u, std::thread::hardware_concurrency() / 2u), 64, true, 10000) - { - } - - SearchOptions(const int depth, - const usize threadCount, - const usize tableSizeMb, - const bool quietSearch, + { + } + + SearchOptions(const i32 depth, + const usize threadCount, + const usize tableSizeMb, + const bool quietSearch, const i64 searchTime = {}) noexcept : _depth(std::clamp(depth, 2, MAX_DEPTH)), _threadCount(std::clamp(threadCount, 1u, std::thread::hardware_concurrency())), diff --git a/ChessAndroid/app/src/main/cpp/chess/Tests.cpp b/ChessAndroid/app/src/main/cpp/chess/Tests.cpp index 4a0560a..58acdf7 100644 --- a/ChessAndroid/app/src/main/cpp/chess/Tests.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/Tests.cpp @@ -1,365 +1,374 @@ -#include "Tests.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "Board.h" -#include "MoveGen.h" -#include "algorithm/Evaluation.h" - -namespace Tests -{ - // region Evaluation - - static Board mirrorBoard(const Board &board) - { - static constexpr std::array MirrorSquare = { - 56, 57, 58, 59, 60, 61, 62, 63, - 48, 49, 50, 51, 52, 53, 54, 55, - 40, 41, 42, 43, 44, 45, 46, 47, - 32, 33, 34, 35, 36, 37, 38, 39, - 24, 25, 26, 27, 28, 29, 30, 31, - 16, 17, 18, 19, 20, 21, 22, 23, - 8, 9, 10, 11, 12, 13, 14, 15, - 0, 1, 2, 3, 4, 5, 6, 7, - }; - - Board result; - - result.colorToMove = ~board.colorToMove; - for (u8 sq{}; sq < SQUARE_NB; ++sq) - { - const auto &piece = board.data[sq]; - if (piece) - result.data[MirrorSquare[sq]] = ~piece; - } - - // Castling - const u8 originalRights = board.getCastlingRights(); - u8 &rights = result.state.castlingRights; - rights |= (originalRights & 0b111u) << 3u; // Black -> White - rights |= originalRights >> 3u; // White -> Black - - if (board.getEnPassantSq() != SQ_NONE) - result.state.enPassantSq = toSquare(MirrorSquare.at(u8(board.getEnPassantSq()))); - - result.updatePieceList(); - result.updateNonPieceBitboards(); - result.state.fiftyMoveRule = board.state.fiftyMoveRule; - result.state.kingAttackers = result.generateAttackers(result.getKingSq(result.colorToMove)) - & result.getPieces(~result.colorToMove); - result.computeCheckInfo(); - - return result; - } - - std::string runEvaluationTests() noexcept - { - static constexpr std::array Positions = { - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq", - "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq", - "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq", - "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ", - "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w", - "8/8/p1p5/1p5p/1P5p/8/PPP2K1p/4R1rk w", - "1q1k4/2Rr4/8/2Q3K1/8/8/8/8 w", - "7k/5K2/5P1p/3p4/6P1/3p4/8/8 w", - "8/6B1/p5p1/Pp4kp/1P5r/5P1Q/4q1PK/8 w", - "8/8/1p1r1k2/p1pPN1p1/P3KnP1/1P6/8/3R4 b", - "1r2r1k1/1pqbbppp/p2p1n2/4p3/P3PP2/2N1BB2/1PP2QPP/R4R1K b", - "r1bk1bnr/ppp2ppp/8/4n3/2P5/P3B3/1P3PPP/RN2KBNR w KQ", - "8/7p/3k2p1/6P1/4KP2/8/7P/8 w", - "r1b1r1k1/1p1n1pbp/2p1n1p1/q1P1p3/4P3/1PN1BNPP/2Q2PB1/1R1R2K1 w", - "1rN1r1k1/1pq2pp1/2p1nn1p/p2p1B2/3P4/4P2P/PPQ1NPP1/2R2RK1 b", - "3r1rk1/p1q2pbp/1np1p1p1/1p2P3/5P2/2N2Q1P/PPP3P1/3RRBK1 b", - "r2r2k1/p3ppbp/1p4p1/3p4/3P4/2P1P2P/P3BPP1/2R2RK1 w", - "2rq3r/pb1pbkpp/1p2pp2/n1P5/2P5/QP2BNPB/P3PP1P/3R1RK1 w", - "1r1q1rk1/pp1bbppp/2n1p1n1/4P3/2BpN3/3P1N2/PP2QPPP/R1B1R1K1 b", - "3r2k1/2p2ppp/1p1br3/pPn5/3PP3/P7/1B1N2PP/R3R1K1 w", - "r4rk1/p2qn1bp/1pnp2p1/2p2p2/4PP1N/2PPB3/PP2QN1P/R4RK1 b", - "r3r1k1/1b1n1p2/1q1p1n1p/2p1p2P/p1P3p1/P1QNPPB1/1P2B1P1/2KR3R w", - "1rb1nrk1/2q1bppp/p1n1p3/2p1P3/2Pp1PP1/3P1NN1/P5BP/R1BQ1RK1 w", - "r3r1k1/ppqbbpp1/2pp1nnp/3Pp3/2P1P3/5N1P/PPBN1PP1/R1BQR1K1 w", - "5r1k/1q2rnpp/p4p2/1pp5/6Q1/1P3P2/PBP3PP/3RR1K1 w", - "2r2k2/5p2/2Bp1b1r/2qPp1pp/PpN1P3/1P2Q3/5PPP/4R1K1 w", - "1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b", - "3r1k2/4npp1/1ppr3p/p6P/P2PPPP1/1NR5/5K2/2R5 w", - "2q1rr1k/3bbnnp/p2p1pp1/2pPp3/PpP1P1P1/1P2BNNP/2BQ1PRK/7R b", - "rnbqkb1r/p3pppp/1p6/2ppP3/3N4/2P5/PPP1QPPP/R1B1KB1R w KQkq", - "r1b2rk1/2q1b1pp/p2ppn2/1p6/3QP3/1BN1B3/PPP3PP/R4RK1 w", - "2r3k1/pppR1pp1/4p3/4P1P1/5P2/1P4K1/P1P5/8 w", - "1nk1r1r1/pp2n1pp/4p3/q2pPp1N/b1pP1P2/B1P2R2/2P1B1PP/R2Q2K1 w", - "4b3/p3kp2/6p1/3pP2p/2pP1P2/4K1P1/P3N2P/8 w", - "2kr1bnr/pbpq4/2n1pp2/3p3p/3P1P1B/2N2N1Q/PPP3PP/2KR1B1R w", - "3rr1k1/pp3pp1/1qn2np1/8/3p4/PP1R1P2/2P1NQPP/R1B3K1 b", - "2r1nrk1/p2q1ppp/bp1p4/n1pPp3/P1P1P3/2PBB1N1/4QPPP/R4RK1 w", - "r3r1k1/ppqb1ppp/8/4p1NQ/8/2P5/PP3PPP/R3R1K1 b", - "r2q1rk1/4bppp/p2p4/2pP4/3pP3/3Q4/PP1B1PPP/R3R1K1 w", - "rnb2r1k/pp2p2p/2pp2p1/q2P1p2/8/1Pb2NP1/PB2PPBP/R2Q1RK1 w", - "2r3k1/1p2q1pp/2b1pr2/p1pp4/6Q1/1P1PP1R1/P1PN2PP/5RK1 w", - "r1bqkb1r/4npp1/p1p4p/1p1pP1B1/8/1B6/PPPN1PPP/R2Q1RK1 w kq", - "r2q1rk1/1ppnbppp/p2p1nb1/3Pp3/2P1P1P1/2N2N1P/PPB1QP2/R1B2RK1 b", - "r1bq1rk1/pp2ppbp/2np2p1/2n5/P3PP2/N1P2N2/1PB3PP/R1B1QRK1 b", - "3rr3/2pq2pk/p2p1pnp/8/2QBPP2/1P6/P5PP/4RRK1 b", - "r4k2/pb2bp1r/1p1qp2p/3pNp2/3P1P2/2N3P1/PPP1Q2P/2KRR3 w", - "3rn2k/ppb2rpp/2ppqp2/5N2/2P1P3/1P5Q/PB3PPP/3RR1K1 w", - "2r2rk1/1bqnbpp1/1p1ppn1p/pP6/N1P1P3/P2B1N1P/1B2QPP1/R2R2K1 b", - "r1bqk2r/pp2bppp/2p5/3pP3/P2Q1P2/2N1B3/1PP3PP/R4RK1 b kq", - "r2qnrnk/p2b2b1/1p1p2pp/2pPpp2/1PP1P3/PRNBB3/3QNPPP/5RK1 w", - "4Q3/6pk/2pq4/3p4/1p1P3p/1P1K1P2/1PP3P1/8 b", - "8/5pk1/4p3/7Q/8/3q4/KP6/8 b", - "r3bb2/P1q3k1/Q2p3p/2pPp1pP/2B1P3/2B5/6P1/R5K1 w", - "r1b5/p2k1r1p/3P2pP/1ppR4/2P2p2/2P5/P1B4P/4R1K1 w", - "6r1/1p3k2/pPp4R/K1P1p1p1/1P2Pp1p/5P1P/6P1/8 w", - "1k2b3/4bpp1/p2pp1P1/1p3P2/2q1P3/4B3/PPPQN2r/1K1R4 w", - "2kr3r/ppp1qpp1/2p5/2b2b2/2P1pPP1/1P2P1p1/PBQPB3/RN2K1R1 b", - "6k1/2q3p1/1n2Pp1p/pBp2P2/Pp2P3/1P1Q1KP1/8/8 w", - "5r2/pp1RRrk1/4Qq1p/1PP3p1/8/4B3/1b3P1P/6K1 w", - "6k1/1q2rpp1/p6p/P7/1PB1n3/5Q2/6PP/5R1K w", - "3r2k1/p6p/b2r2p1/2qPQp2/2P2P2/8/6BP/R4R1K w", - "8/6Bp/6p1/2k1p3/4PPP1/1pb4P/8/2K5 b", - "2r1rbk1/p1Bq1ppp/Ppn1b3/1Npp4/B7/3P2Q1/1PP2PPP/R4RK1 w", - "r4rk1/ppq3pp/2p1Pn2/4p1Q1/8/2N5/PP4PP/2KR1R2 w", - "6k1/p4pp1/Pp2r3/1QPq3p/8/6P1/2P2P1P/1R4K1 w", - "8/2k5/2p5/2pb2K1/pp4P1/1P1R4/P7/8 b", - "2r5/1r5k/1P3p2/PR2pP1p/4P2p/2p1BP2/1p2n3/4R2K b", - "8/1R2P3/6k1/3B4/2P2P2/1p2r3/1Kb4p/8 w", - "1q1r3k/3P1pp1/ppBR1n1p/4Q2P/P4P2/8/5PK1/8 w", - "6k1/5pp1/pb1r3p/8/2q1P3/1p3N1P/1P3PP1/2R1Q1K1 b", - "8/Bpk5/8/P2K4/8/8/8/8 w", - "1r6/5k2/p4p1K/5R2/7P/8/6P1/8 w", - "8/6k1/p4p2/P3q2p/7P/5Q2/5PK1/8 w", - "8/8/6p1/3Pkp2/4P3/2K5/6P1/n7 w", - "1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b", - "r1k2/4npp1/1ppr3p/p6P/P2PPPP1/1NR5/5K2/2R5 w", - "q1rr1k/3bbnnp/p2p1pp1/2pPp3/PpP1P1P1/1P2BNNP/2BQ1PRK/7R b", - "r1b2rk1/2q1b1pp/p2ppn2/1p6/3QP3/1BN1B3/PPP3PP/R4RK1 w", - "2r3k1/pppR1pp1/4p3/4P1P1/5P2/1P4K1/P1P5/8 w", - "1nk1r1r1/pp2n1pp/4p3/q2pPp1N/b1pP1P2/B1P2R2/2P1B1PP/R2Q2K1 w", - "4b3/p3kp2/6p1/3pP2p/2pP1P2/4K1P1/P3N2P/8 w", - "2kr1bnr/pbpq4/2n1pp2/3p3p/3P1P1B/2N2N1Q/PPP3PP/2KR1B1R w", - "3rr1k1/pp3pp1/1qn2np1/8/3p4/PP1R1P2/2P1NQPP/R1B3K1 b", - "2r1nrk1/p2q1ppp/bp1p4/n1pPp3/P1P1P3/2PBB1N1/4QPPP/R4RK1 w", - "r3r1k1/ppqb1ppp/8/4p1NQ/8/2P5/PP3PPP/R3R1K1 b", - "r2q1rk1/4bppp/p2p4/2pP4/3pP3/3Q4/PP1B1PPP/R3R1K1 w", - "rnb2r1k/pp2p2p/2pp2p1/q2P1p2/8/1Pb2NP1/PB2PPBP/R2Q1RK1 w", - "2r3k1/1p2q1pp/2b1pr2/p1pp4/6Q1/1P1PP1R1/P1PN2PP/5RK1 w", - "r1bqkb1r/4npp1/p1p4p/1p1pP1B1/8/1B6/PPPN1PPP/R2Q1RK1 w kq", - "r2q1rk1/1ppnbppp/p2p1nb1/3Pp3/2P1P1P1/2N2N1P/PPB1QP2/R1B2RK1 b", - "r1bq1rk1/pp2ppbp/2np2p1/2n5/P3PP2/N1P2N2/1PB3PP/R1B1QRK1 b", - "3rr3/2pq2pk/p2p1pnp/8/2QBPP2/1P6/P5PP/4RRK1 b", - "r4k2/pb2bp1r/1p1qp2p/3pNp2/3P1P2/2N3P1/PPP1Q2P/2KRR3 w", - "3rn2k/ppb2rpp/2ppqp2/5N2/2P1P3/1P5Q/PB3PPP/3RR1K1 w", - "2r2rk1/1bqnbpp1/1p1ppn1p/pP6/N1P1P3/P2B1N1P/1B2QPP1/R2R2K1 b", - "r1bqk2r/pp2bppp/2p5/3pP3/P2Q1P2/2N1B3/1PP3PP/R4RK1 b kq", - "r2qnrnk/p2b2b1/1p1p2pp/2pPpp2/1PP1P3/PRNBB3/3QNPPP/5RK1 w" - }; - - std::ostringstream output; - - for (auto &&pos : Positions) - { - Board board; - board.setToFen(pos); - const Board mirroredBoard = mirrorBoard(board); - - const int boardEval = Evaluation::invertedValue(board); - const int mirroredBoardEval = Evaluation::invertedValue(mirroredBoard); - - if (boardEval != mirroredBoardEval) - { - output << "Evaluation Asymmetrical for: " << pos << '\n' - << board.toString() - << mirroredBoard.toString() << '\n' - << Evaluation::traceValue(board) - << Evaluation::traceValue(mirroredBoard) - << '\n'; - break; - } - } - - return output.str(); - } - - // endregion Evaluation - - // region Perft - - struct PerftInfo - { - u64 nodes{}; - u64 captures{}; - u64 enPassant{}; - u64 castles{}; - u64 promotions{}; - u64 checks{}; - u64 doubleChecks{}; - - constexpr PerftInfo &operator+=(const PerftInfo &rhs) noexcept - { - nodes += rhs.nodes; - captures += rhs.captures; - enPassant += rhs.enPassant; - castles += rhs.castles; - promotions += rhs.promotions; - checks += rhs.checks; - doubleChecks += rhs.doubleChecks; - return *this; - } - }; - - static void perft(Board &board, PerftInfo &info, const unsigned depth) - { - if (board.state.fiftyMoveRule > 99) - return; - - if (depth == 0) - { - const auto flags = board.state.getMove().flags(); - ++info.nodes; - info.captures += flags.capture() | flags.enPassant(); - info.enPassant += flags.enPassant(); - info.castles += flags.kSideCastle() | flags.qSideCastle(); - info.promotions += flags.promotion(); - const auto kingAttackers = board.getKingAttackers(); - info.checks += kingAttackers.notEmpty(); - info.doubleChecks += kingAttackers.several(); - return; - } - - MoveList moveList(board); - moveList.keepLegalMoves(); - - for (const Move move : moveList) - { - board.makeMove(move); - perft(board, info, depth - 1); - board.undoMove(); - } - } - - static PerftInfo basePerft(std::ostringstream &out, Board &board, const unsigned depth) - { - PerftInfo info{}; - - MoveList moveList(board); - moveList.keepLegalMoves(); - - for (const Move move : moveList) - { - PerftInfo localInfo{}; - board.makeMove(move); - perft(board, localInfo, depth - 1); - board.undoMove(); - - info += localInfo; - out << move.toString() << ": " << localInfo.nodes << '\n'; - } - - return info; - } - - static void perftWrapper(std::string_view tag, std::string_view fen, const std::initializer_list perftResults) - { - Board board; - board.setToFen(std::string(fen)); - - std::vector perftVector(perftResults); - - constexpr auto DepthW = 5; - constexpr auto ColumnW = 12; - constexpr std::string_view Pipe = " | "; - - const auto displayRow = [&](const i32 depth, const double time, const PerftInfo &info) - { - using std::setw; - const auto expectedNodes = perftVector[depth]; - const auto wrongResult = expectedNodes != info.nodes; - const auto nodes = (wrongResult ? (std::string("!!!") + std::to_string(expectedNodes)) : std::to_string(expectedNodes)); - - std::cout << "| " << std::setfill(' ') << std::fixed << std::setprecision(1) - << setw(DepthW) << depth << Pipe - << setw(ColumnW) << time << Pipe - << setw(ColumnW) << expectedNodes << Pipe - << setw(ColumnW) << nodes << Pipe - << setw(ColumnW) << info.captures << Pipe - << setw(ColumnW) << info.enPassant << Pipe - << setw(ColumnW) << info.castles << Pipe - << setw(ColumnW) << info.promotions << Pipe - << setw(ColumnW) << info.checks << Pipe - << setw(ColumnW) << info.doubleChecks << Pipe; - }; - - std::cout << tag << ':' << '\n'; - std::cout << "| " << std::setfill(' ') - << std::setw(DepthW) << "Depth" << Pipe - << std::setw(ColumnW) << "Seconds" << Pipe - << std::setw(ColumnW) << "Expected" << Pipe - << std::setw(ColumnW) << "Nodes" << Pipe - << std::setw(ColumnW) << "E.P." << Pipe - << std::setw(ColumnW) << "Captures" << Pipe - << std::setw(ColumnW) << "Castles" << Pipe - << std::setw(ColumnW) << "Promotions" << Pipe - << std::setw(ColumnW) << "Checks" << Pipe - << std::setw(ColumnW) << "DoubleChecks" << Pipe << '\n'; - - for (unsigned depth = 1; depth < perftVector.size(); ++depth) - { - std::ostringstream out; - const auto startTime = std::chrono::high_resolution_clock::now(); - const PerftInfo info = basePerft(out, board, depth); - - const auto endTime = std::chrono::high_resolution_clock::now(); - const auto timeNeeded = - std::chrono::duration(endTime - startTime).count() / 1000; - - displayRow(depth, timeNeeded, info); - - /*if (depth == perftVector.size() - 1 || info.nodes != expectedNodeCount) - { - std::cout << '\n' << out.str(); - break; - }*/ - - std::cout << std::endl; - } - - std::cout.flush(); - } - - void runPerftTests() noexcept - { - perftWrapper("Position 1", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq", { - 1, 20, 400, 8902, 197281, 4865609, 119060324 - }); - - perftWrapper("Position 2", "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq", { - 1, 48, 2039, 97862, 4085603, 193690690 - }); - - perftWrapper("Position 3", "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w", { - 1, 14, 191, 2812, 43238, 674624, 11030083, 178633661 - }); - - perftWrapper("Position 4", "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", { - 1, 6, 264, 9467, 422333, 15833292 - }); - - perftWrapper("Position 5", "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", { - 1, 44, 1486, 62379, 2103487, 89941194 - }); - - perftWrapper("Position 6", "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", { - 1, 46, 2079, 89890, 3894594, 164075551 - }); - - std::cout << "Perft tests execution finished" << std::endl; - } - - // endregion Perft -} +#include "Tests.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "Board.h" +#include "MoveGen.h" +#include "algorithm/Evaluation.h" + +namespace Tests +{ + // region Evaluation + + static Board mirrorBoard(const Board &board) + { + static constexpr std::array MirrorSquare = { + 56, 57, 58, 59, 60, 61, 62, 63, + 48, 49, 50, 51, 52, 53, 54, 55, + 40, 41, 42, 43, 44, 45, 46, 47, + 32, 33, 34, 35, 36, 37, 38, 39, + 24, 25, 26, 27, 28, 29, 30, 31, + 16, 17, 18, 19, 20, 21, 22, 23, + 8, 9, 10, 11, 12, 13, 14, 15, + 0, 1, 2, 3, 4, 5, 6, 7, + }; + + Board result; + + result.colorToMove = ~board.colorToMove; + for (u8 sq{}; sq < SQUARE_NB; ++sq) + { + const auto &piece = board.data[sq]; + if (piece) + result.data[MirrorSquare[sq]] = ~piece; + } + + // Castling + const u8 originalRights = board.getCastlingRights(); + u8 &rights = result.state.castlingRights; + rights |= (originalRights & 0b111u) << 3u; // Black -> White + rights |= originalRights >> 3u; // White -> Black + + if (board.getEnPassantSq() != SQ_NONE) + result.state.enPassantSq = toSquare(MirrorSquare.at(u8(board.getEnPassantSq()))); + + result.updatePieceList(); + result.updateNonPieceBitboards(); + result.state.fiftyMoveRule = board.state.fiftyMoveRule; + result.state.kingAttackers = result.generateAttackers(result.getKingSq(result.colorToMove)) + & result.getPieces(~result.colorToMove); + result.computeCheckInfo(); + + return result; + } + + std::string runEvaluationTests() noexcept + { + static constexpr std::array Positions = { + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq", + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq", + "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq", + "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ", + "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w", + "8/8/p1p5/1p5p/1P5p/8/PPP2K1p/4R1rk w", + "1q1k4/2Rr4/8/2Q3K1/8/8/8/8 w", + "7k/5K2/5P1p/3p4/6P1/3p4/8/8 w", + "8/6B1/p5p1/Pp4kp/1P5r/5P1Q/4q1PK/8 w", + "8/8/1p1r1k2/p1pPN1p1/P3KnP1/1P6/8/3R4 b", + "1r2r1k1/1pqbbppp/p2p1n2/4p3/P3PP2/2N1BB2/1PP2QPP/R4R1K b", + "r1bk1bnr/ppp2ppp/8/4n3/2P5/P3B3/1P3PPP/RN2KBNR w KQ", + "8/7p/3k2p1/6P1/4KP2/8/7P/8 w", + "r1b1r1k1/1p1n1pbp/2p1n1p1/q1P1p3/4P3/1PN1BNPP/2Q2PB1/1R1R2K1 w", + "1rN1r1k1/1pq2pp1/2p1nn1p/p2p1B2/3P4/4P2P/PPQ1NPP1/2R2RK1 b", + "3r1rk1/p1q2pbp/1np1p1p1/1p2P3/5P2/2N2Q1P/PPP3P1/3RRBK1 b", + "r2r2k1/p3ppbp/1p4p1/3p4/3P4/2P1P2P/P3BPP1/2R2RK1 w", + "2rq3r/pb1pbkpp/1p2pp2/n1P5/2P5/QP2BNPB/P3PP1P/3R1RK1 w", + "1r1q1rk1/pp1bbppp/2n1p1n1/4P3/2BpN3/3P1N2/PP2QPPP/R1B1R1K1 b", + "3r2k1/2p2ppp/1p1br3/pPn5/3PP3/P7/1B1N2PP/R3R1K1 w", + "r4rk1/p2qn1bp/1pnp2p1/2p2p2/4PP1N/2PPB3/PP2QN1P/R4RK1 b", + "r3r1k1/1b1n1p2/1q1p1n1p/2p1p2P/p1P3p1/P1QNPPB1/1P2B1P1/2KR3R w", + "1rb1nrk1/2q1bppp/p1n1p3/2p1P3/2Pp1PP1/3P1NN1/P5BP/R1BQ1RK1 w", + "r3r1k1/ppqbbpp1/2pp1nnp/3Pp3/2P1P3/5N1P/PPBN1PP1/R1BQR1K1 w", + "5r1k/1q2rnpp/p4p2/1pp5/6Q1/1P3P2/PBP3PP/3RR1K1 w", + "2r2k2/5p2/2Bp1b1r/2qPp1pp/PpN1P3/1P2Q3/5PPP/4R1K1 w", + "1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b", + "3r1k2/4npp1/1ppr3p/p6P/P2PPPP1/1NR5/5K2/2R5 w", + "2q1rr1k/3bbnnp/p2p1pp1/2pPp3/PpP1P1P1/1P2BNNP/2BQ1PRK/7R b", + "rnbqkb1r/p3pppp/1p6/2ppP3/3N4/2P5/PPP1QPPP/R1B1KB1R w KQkq", + "r1b2rk1/2q1b1pp/p2ppn2/1p6/3QP3/1BN1B3/PPP3PP/R4RK1 w", + "2r3k1/pppR1pp1/4p3/4P1P1/5P2/1P4K1/P1P5/8 w", + "1nk1r1r1/pp2n1pp/4p3/q2pPp1N/b1pP1P2/B1P2R2/2P1B1PP/R2Q2K1 w", + "4b3/p3kp2/6p1/3pP2p/2pP1P2/4K1P1/P3N2P/8 w", + "2kr1bnr/pbpq4/2n1pp2/3p3p/3P1P1B/2N2N1Q/PPP3PP/2KR1B1R w", + "3rr1k1/pp3pp1/1qn2np1/8/3p4/PP1R1P2/2P1NQPP/R1B3K1 b", + "2r1nrk1/p2q1ppp/bp1p4/n1pPp3/P1P1P3/2PBB1N1/4QPPP/R4RK1 w", + "r3r1k1/ppqb1ppp/8/4p1NQ/8/2P5/PP3PPP/R3R1K1 b", + "r2q1rk1/4bppp/p2p4/2pP4/3pP3/3Q4/PP1B1PPP/R3R1K1 w", + "rnb2r1k/pp2p2p/2pp2p1/q2P1p2/8/1Pb2NP1/PB2PPBP/R2Q1RK1 w", + "2r3k1/1p2q1pp/2b1pr2/p1pp4/6Q1/1P1PP1R1/P1PN2PP/5RK1 w", + "r1bqkb1r/4npp1/p1p4p/1p1pP1B1/8/1B6/PPPN1PPP/R2Q1RK1 w kq", + "r2q1rk1/1ppnbppp/p2p1nb1/3Pp3/2P1P1P1/2N2N1P/PPB1QP2/R1B2RK1 b", + "r1bq1rk1/pp2ppbp/2np2p1/2n5/P3PP2/N1P2N2/1PB3PP/R1B1QRK1 b", + "3rr3/2pq2pk/p2p1pnp/8/2QBPP2/1P6/P5PP/4RRK1 b", + "r4k2/pb2bp1r/1p1qp2p/3pNp2/3P1P2/2N3P1/PPP1Q2P/2KRR3 w", + "3rn2k/ppb2rpp/2ppqp2/5N2/2P1P3/1P5Q/PB3PPP/3RR1K1 w", + "2r2rk1/1bqnbpp1/1p1ppn1p/pP6/N1P1P3/P2B1N1P/1B2QPP1/R2R2K1 b", + "r1bqk2r/pp2bppp/2p5/3pP3/P2Q1P2/2N1B3/1PP3PP/R4RK1 b kq", + "r2qnrnk/p2b2b1/1p1p2pp/2pPpp2/1PP1P3/PRNBB3/3QNPPP/5RK1 w", + "4Q3/6pk/2pq4/3p4/1p1P3p/1P1K1P2/1PP3P1/8 b", + "8/5pk1/4p3/7Q/8/3q4/KP6/8 b", + "r3bb2/P1q3k1/Q2p3p/2pPp1pP/2B1P3/2B5/6P1/R5K1 w", + "r1b5/p2k1r1p/3P2pP/1ppR4/2P2p2/2P5/P1B4P/4R1K1 w", + "6r1/1p3k2/pPp4R/K1P1p1p1/1P2Pp1p/5P1P/6P1/8 w", + "1k2b3/4bpp1/p2pp1P1/1p3P2/2q1P3/4B3/PPPQN2r/1K1R4 w", + "2kr3r/ppp1qpp1/2p5/2b2b2/2P1pPP1/1P2P1p1/PBQPB3/RN2K1R1 b", + "6k1/2q3p1/1n2Pp1p/pBp2P2/Pp2P3/1P1Q1KP1/8/8 w", + "5r2/pp1RRrk1/4Qq1p/1PP3p1/8/4B3/1b3P1P/6K1 w", + "6k1/1q2rpp1/p6p/P7/1PB1n3/5Q2/6PP/5R1K w", + "3r2k1/p6p/b2r2p1/2qPQp2/2P2P2/8/6BP/R4R1K w", + "8/6Bp/6p1/2k1p3/4PPP1/1pb4P/8/2K5 b", + "2r1rbk1/p1Bq1ppp/Ppn1b3/1Npp4/B7/3P2Q1/1PP2PPP/R4RK1 w", + "r4rk1/ppq3pp/2p1Pn2/4p1Q1/8/2N5/PP4PP/2KR1R2 w", + "6k1/p4pp1/Pp2r3/1QPq3p/8/6P1/2P2P1P/1R4K1 w", + "8/2k5/2p5/2pb2K1/pp4P1/1P1R4/P7/8 b", + "2r5/1r5k/1P3p2/PR2pP1p/4P2p/2p1BP2/1p2n3/4R2K b", + "8/1R2P3/6k1/3B4/2P2P2/1p2r3/1Kb4p/8 w", + "1q1r3k/3P1pp1/ppBR1n1p/4Q2P/P4P2/8/5PK1/8 w", + "6k1/5pp1/pb1r3p/8/2q1P3/1p3N1P/1P3PP1/2R1Q1K1 b", + "8/Bpk5/8/P2K4/8/8/8/8 w", + "1r6/5k2/p4p1K/5R2/7P/8/6P1/8 w", + "8/6k1/p4p2/P3q2p/7P/5Q2/5PK1/8 w", + "8/8/6p1/3Pkp2/4P3/2K5/6P1/n7 w", + "1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b", + "r1k2/4npp1/1ppr3p/p6P/P2PPPP1/1NR5/5K2/2R5 w", + "q1rr1k/3bbnnp/p2p1pp1/2pPp3/PpP1P1P1/1P2BNNP/2BQ1PRK/7R b", + "r1b2rk1/2q1b1pp/p2ppn2/1p6/3QP3/1BN1B3/PPP3PP/R4RK1 w", + "2r3k1/pppR1pp1/4p3/4P1P1/5P2/1P4K1/P1P5/8 w", + "1nk1r1r1/pp2n1pp/4p3/q2pPp1N/b1pP1P2/B1P2R2/2P1B1PP/R2Q2K1 w", + "4b3/p3kp2/6p1/3pP2p/2pP1P2/4K1P1/P3N2P/8 w", + "2kr1bnr/pbpq4/2n1pp2/3p3p/3P1P1B/2N2N1Q/PPP3PP/2KR1B1R w", + "3rr1k1/pp3pp1/1qn2np1/8/3p4/PP1R1P2/2P1NQPP/R1B3K1 b", + "2r1nrk1/p2q1ppp/bp1p4/n1pPp3/P1P1P3/2PBB1N1/4QPPP/R4RK1 w", + "r3r1k1/ppqb1ppp/8/4p1NQ/8/2P5/PP3PPP/R3R1K1 b", + "r2q1rk1/4bppp/p2p4/2pP4/3pP3/3Q4/PP1B1PPP/R3R1K1 w", + "rnb2r1k/pp2p2p/2pp2p1/q2P1p2/8/1Pb2NP1/PB2PPBP/R2Q1RK1 w", + "2r3k1/1p2q1pp/2b1pr2/p1pp4/6Q1/1P1PP1R1/P1PN2PP/5RK1 w", + "r1bqkb1r/4npp1/p1p4p/1p1pP1B1/8/1B6/PPPN1PPP/R2Q1RK1 w kq", + "r2q1rk1/1ppnbppp/p2p1nb1/3Pp3/2P1P1P1/2N2N1P/PPB1QP2/R1B2RK1 b", + "r1bq1rk1/pp2ppbp/2np2p1/2n5/P3PP2/N1P2N2/1PB3PP/R1B1QRK1 b", + "3rr3/2pq2pk/p2p1pnp/8/2QBPP2/1P6/P5PP/4RRK1 b", + "r4k2/pb2bp1r/1p1qp2p/3pNp2/3P1P2/2N3P1/PPP1Q2P/2KRR3 w", + "3rn2k/ppb2rpp/2ppqp2/5N2/2P1P3/1P5Q/PB3PPP/3RR1K1 w", + "2r2rk1/1bqnbpp1/1p1ppn1p/pP6/N1P1P3/P2B1N1P/1B2QPP1/R2R2K1 b", + "r1bqk2r/pp2bppp/2p5/3pP3/P2Q1P2/2N1B3/1PP3PP/R4RK1 b kq", + "r2qnrnk/p2b2b1/1p1p2pp/2pPpp2/1PP1P3/PRNBB3/3QNPPP/5RK1 w" + }; + + std::ostringstream output; + + for (auto &&pos : Positions) + { + Board board; + board.setToFen(pos); + const Board mirroredBoard = mirrorBoard(board); + + const int boardEval = Evaluation::invertedValue(board); + const int mirroredBoardEval = Evaluation::invertedValue(mirroredBoard); + + if (boardEval != mirroredBoardEval) + { + output << "Evaluation Asymmetrical for: " << pos << '\n' + << board.toString() + << mirroredBoard.toString() << '\n' + << Evaluation::traceValue(board) + << Evaluation::traceValue(mirroredBoard) + << '\n'; + break; + } + } + + return output.str(); + } + + // endregion Evaluation + + // region Perft + + struct PerftInfo + { + u64 nodes{}; + u64 captures{}; + u64 enPassant{}; + u64 castles{}; + u64 promotions{}; + u64 checks{}; + u64 doubleChecks{}; + + constexpr PerftInfo &operator+=(const PerftInfo &rhs) noexcept + { + nodes += rhs.nodes; + captures += rhs.captures; + enPassant += rhs.enPassant; + castles += rhs.castles; + promotions += rhs.promotions; + checks += rhs.checks; + doubleChecks += rhs.doubleChecks; + return *this; + } + }; + + static void perft(Board &board, PerftInfo &info, const unsigned depth) + { + if (board.state.fiftyMoveRule > 99) + return; + + if (depth == 0) + { + const auto flags = board.state.getMove().flags(); + ++info.nodes; + info.captures += flags.capture() | flags.enPassant(); + info.enPassant += flags.enPassant(); + info.castles += flags.kSideCastle() | flags.qSideCastle(); + info.promotions += flags.promotion(); + const auto kingAttackers = board.getKingAttackers(); + info.checks += kingAttackers.notEmpty(); + info.doubleChecks += kingAttackers.several(); + return; + } + + MoveList moveList(board); + moveList.keepLegalMoves(); + + for (const Move move : moveList) + { + board.makeMove(move); + perft(board, info, depth - 1); + board.undoMove(); + } + } + + static PerftInfo basePerft(std::ostringstream &out, Board &board, const unsigned depth) + { + PerftInfo info{}; + + MoveList moveList(board); + moveList.keepLegalMoves(); + + for (const Move move : moveList) + { + PerftInfo localInfo{}; + board.makeMove(move); + perft(board, localInfo, depth - 1); + board.undoMove(); + + info += localInfo; + out << move.toString() << ": " << localInfo.nodes << '\n'; + } + + return info; + } + + static void perftWrapper(std::string_view tag, std::string_view fen, const std::vector perftVector) + { + using std::setw; + + Board board; + board.setToFen(std::string(fen)); + + constexpr auto DepthW = 5; + constexpr auto ColumnW = 12; + constexpr std::string_view Pipe = " | "; + + const auto displayRow = [&](const i32 depth, const double time, const PerftInfo &info) + { + + const auto expectedNodes = perftVector[depth]; + const auto wrongResult = expectedNodes != info.nodes; + const auto nodes = (wrongResult ? (std::string("!!!") + std::to_string(info.nodes)) : std::to_string(info.nodes)); + + std::cout << "| " << std::setfill(' ') << std::fixed << std::setprecision(1) + << setw(DepthW) << depth << Pipe + << setw(ColumnW) << time << Pipe + << setw(ColumnW) << expectedNodes << Pipe + << setw(ColumnW) << nodes << Pipe + << setw(ColumnW) << info.captures << Pipe + << setw(ColumnW) << info.enPassant << Pipe + << setw(ColumnW) << info.castles << Pipe + << setw(ColumnW) << info.promotions << Pipe + << setw(ColumnW) << info.checks << Pipe + << setw(ColumnW) << info.doubleChecks << Pipe; + }; + + std::cout << tag << ':' << '\n'; + std::cout << "| " << std::setfill(' ') + << setw(DepthW) << "Depth" << Pipe + << setw(ColumnW) << "Seconds" << Pipe + << setw(ColumnW) << "Expected" << Pipe + << setw(ColumnW) << "Nodes" << Pipe + << setw(ColumnW) << "E.P." << Pipe + << setw(ColumnW) << "Captures" << Pipe + << setw(ColumnW) << "Castles" << Pipe + << setw(ColumnW) << "Promotions" << Pipe + << setw(ColumnW) << "Checks" << Pipe + << setw(ColumnW) << "DoubleChecks" << Pipe << '\n'; + + for (unsigned depth = 1; depth < perftVector.size(); ++depth) + { + std::ostringstream out; + const auto startTime = std::chrono::high_resolution_clock::now(); + const PerftInfo info = basePerft(out, board, depth); + + const auto endTime = std::chrono::high_resolution_clock::now(); + const auto timeNeeded = + std::chrono::duration(endTime - startTime).count() / 1000; + + displayRow(depth, timeNeeded, info); + + if (depth == perftVector.size() - 1 || info.nodes != perftVector[depth]) + { + std::cout << '\n' << out.str(); +// break; + } + + std::cout << std::endl; + } + + std::cout.flush(); + } + + void runPerftTests() noexcept + { + perftWrapper("Position 1", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq", { + 1, 20, 400, 8902, 197281, 4865609, 119060324 + }); + + perftWrapper("Position 2", "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq", { + 1, 48, 2039, 97862, 4085603, 193690690 + }); + + perftWrapper("Position 3", "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w", { + 1, 14, 191, 2812, 43238, 674624, 11030083, 178633661 + }); + + /*perftWrapper("Position 4", "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/P2P2PP/r2Q1R1K w kq - 0 2", { + 1, 6, 264, 9467, 422333, 15833292, 706045033 + });*/ + + perftWrapper("Position 4", "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", { + 1, 6, 264, 9467, 422333, 15833292, 706045033 + }); + + perftWrapper("Position 5", "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", { + 1, 44, 1486, 62379, 2103487, 89941194 + }); + + perftWrapper("Position 6", "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", { + 1, 46, 2079, 89890, 3894594, 164075551 + }); + + std::cout << "Perft tests execution finished" << std::endl; + } + + void runPerftForPosition(const std::string &fen, const i32 depth) + { + perftWrapper("Perft", fen, std::vector(depth)); + } + + // endregion Perft +} diff --git a/ChessAndroid/app/src/main/cpp/chess/Tests.h b/ChessAndroid/app/src/main/cpp/chess/Tests.h index 52fd8b1..ca94b5d 100644 --- a/ChessAndroid/app/src/main/cpp/chess/Tests.h +++ b/ChessAndroid/app/src/main/cpp/chess/Tests.h @@ -1,10 +1,14 @@ -#pragma once - -#include - -namespace Tests -{ - std::string runEvaluationTests() noexcept; - - void runPerftTests() noexcept; -} +#pragma once + +#include + +#include "Defs.h" + +namespace Tests +{ + std::string runEvaluationTests() noexcept; + + void runPerftTests() noexcept; + + void runPerftForPosition(const std::string &fen, i32 depth); +} diff --git a/ChessAndroid/app/src/main/cpp/chess/TranspositionTable.cpp b/ChessAndroid/app/src/main/cpp/chess/TranspositionTable.cpp index b05b9a3..62e32f0 100644 --- a/ChessAndroid/app/src/main/cpp/chess/TranspositionTable.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/TranspositionTable.cpp @@ -1,140 +1,141 @@ -#include "TranspositionTable.h" - -#include -#include - -static constexpr u64 MB = 1ull << 20; - -TranspositionTable::TranspositionTable(const usize sizeMb) -{ - setSize(sizeMb); -} - -TranspositionTable::~TranspositionTable() -{ - delete[] _clusters; -} - -void TranspositionTable::prefetch(const u64 zKey) const noexcept -{ - assert(_clusters); - - const auto address = &_clusters[(zKey >> 48u) & _hashMask]; -#ifdef _MSC_VER - _mm_prefetch((char*)address, _MM_HINT_T0); -#else - __builtin_prefetch(address); -#endif -} - -std::optional TranspositionTable::probe(const u64 zKey) const noexcept -{ - assert(_clusters); - - const u16 key16 = zKey >> 48u; - const auto index = zKey & _hashMask; - - assert(index < _size); - auto &clusterEntries = _clusters[index].entries; - - std::shared_lock lock{ _mutexes[index % MUTEX_COUNT] }; - for (usize i = 0; i < CLUSTER_SIZE; ++i) - { - auto &entry = clusterEntries[i]; - if (key16 == entry.key() && currentAge() == entry.age()) - return entry; - } - - return {}; -} - -void TranspositionTable::insert(const u64 zKey, SearchEntry entry) noexcept -{ - assert(_clusters); - - const auto key = entry.key(); - const auto index = zKey & _hashMask; - const auto tableAge = currentAge(); - - assert(index < _size); - auto &clusterEntries = _clusters[index].entries; - auto *toReplace = &clusterEntries.front(); - - std::lock_guard lock{ _mutexes[index % MUTEX_COUNT] }; - - usize i{}; - for (; i < CLUSTER_SIZE && key != clusterEntries[i].key(); ++i) - { - if (toReplace->depth() - (tableAge - toReplace->age()) - >= clusterEntries[i].depth() - (tableAge - clusterEntries[i].generation)) - toReplace = &clusterEntries[i]; - } - - toReplace = (i != CLUSTER_SIZE) ? &clusterEntries[i] : toReplace; - - // Don't overwrite an entry from the same position, unless we have - // an exact bound or depth that is nearly as good as the old one - if (key == toReplace->key() - && entry.bound() != SearchEntry::Bound::EXACT - && entry.depth() < toReplace->depth() - 3) - return; - - entry.setAge(tableAge); - *toReplace = entry; -} - -bool TranspositionTable::setSize(usize sizeMb) -{ - const auto newSize = (sizeMb << 20u) / sizeof(Cluster); - - if (newSize == 0 || _size == newSize) return false; - - _size = newSize; - delete[] _clusters; - _clusters = nullptr; - - while (!_clusters && sizeMb) - { - const auto bytesSize = sizeof(Cluster) * _size; - _clusters = static_cast(operator new[](bytesSize, std::nothrow)); - - if (_clusters) - clear(); - else - { - std::cerr << "Failed to allocate " << sizeMb << "MB for the Transposition Table\n"; - sizeMb /= 2; - _size = (sizeMb << 20u) / sizeof(Cluster); - } - } - - u64 keySize = 16u; - while ((1ull << keySize) * sizeof(Cluster) <= sizeMb * MB / 2) - ++keySize; - - _hashMask = (1ull << keySize) - 1u; - - return true; -} - -void TranspositionTable::update() noexcept -{ - if (_currentAge == SearchEntry::AGE_BITS) - { - clear(); - } else - { - (++_currentAge) &= SearchEntry::AGE_BITS; - } -} - -u8 TranspositionTable::currentAge() const noexcept -{ - return _currentAge; -} - -void TranspositionTable::clear() noexcept -{ - std::memset(_clusters, 0, sizeof(Cluster) * _size); - _currentAge = {}; -} +#include "TranspositionTable.h" + +#include +#include + +#ifdef _MSC_VER +# include +#endif + +static constexpr u64 MB = 1ull << 20; + +TranspositionTable::TranspositionTable(const usize sizeMb) +{ + setSize(sizeMb); +} + +TranspositionTable::~TranspositionTable() +{ + delete[] _clusters; +} + +void TranspositionTable::prefetch(const u64 zKey) const noexcept +{ + assert(_clusters); + + const auto address = &_clusters[(zKey >> 48u) & _hashMask]; +#ifdef _MSC_VER + _mm_prefetch(reinterpret_cast(address), _MM_HINT_T0); +#else + __builtin_prefetch(address); +#endif +} + +std::optional TranspositionTable::probe(const u64 zKey) const noexcept +{ + assert(_clusters); + + const u16 key16 = zKey >> 48u; + const auto index = zKey & _hashMask; + + assert(index < _size); + auto &clusterEntries = _clusters[index].entries; + + for (usize i = 0; i < CLUSTER_SIZE; ++i) + { + auto &entry = clusterEntries[i]; + if (key16 == entry.key() && currentAge() == entry.age()) + return entry; + } + + return {}; +} + +void TranspositionTable::insert(const u64 zKey, SearchEntry entry) noexcept +{ + assert(_clusters); + + const auto key = entry.key(); + const auto index = zKey & _hashMask; + const auto tableAge = currentAge(); + + assert(index < _size); + auto &clusterEntries = _clusters[index].entries; + auto *toReplace = &clusterEntries.front(); + + usize i{}; + for (; i < CLUSTER_SIZE && key != clusterEntries[i].key(); ++i) + { + if (toReplace->depth() - (tableAge - toReplace->age()) + >= clusterEntries[i].depth() - (tableAge - clusterEntries[i].age())) + toReplace = &clusterEntries[i]; + } + + toReplace = (i != CLUSTER_SIZE) ? &clusterEntries[i] : toReplace; + + // Don't overwrite an entry from the same position, unless we have + // an exact bound or depth that is nearly as good as the old one + if (key == toReplace->key() + && entry.bound() != SearchEntry::Bound::EXACT + && entry.depth() < toReplace->depth() - 3) + return; + + entry.setAge(tableAge); + *toReplace = entry; +} + +bool TranspositionTable::setSize(usize sizeMb) +{ + const auto newSize = (sizeMb << 20u) / sizeof(Cluster); + + if (newSize == 0 || _size == newSize) return false; + + _size = newSize; + delete[] _clusters; + _clusters = nullptr; + + while (!_clusters && sizeMb) + { + const auto bytesSize = sizeof(Cluster) * _size; + _clusters = static_cast(operator new[](bytesSize, std::nothrow)); + + if (_clusters) + clear(); + else + { + std::cerr << "Failed to allocate " << sizeMb << "MB for the Transposition Table\n"; + sizeMb /= 2; + _size = (sizeMb << 20u) / sizeof(Cluster); + } + } + + u64 keySize = 16u; + while ((1ull << keySize) * sizeof(Cluster) <= sizeMb * MB / 2) + ++keySize; + + _hashMask = (1ull << keySize) - 1u; + + return true; +} + +void TranspositionTable::update() noexcept +{ + if (_currentAge == SearchEntry::AGE_MASK) + { + clear(); + } else + { + (++_currentAge) &= SearchEntry::AGE_MASK; + } +} + +u8 TranspositionTable::currentAge() const noexcept +{ + return _currentAge; +} + +void TranspositionTable::clear() noexcept +{ + std::memset(_clusters, 0, sizeof(Cluster) * _size); + _currentAge = {}; +} diff --git a/ChessAndroid/app/src/main/cpp/chess/TranspositionTable.h b/ChessAndroid/app/src/main/cpp/chess/TranspositionTable.h index f21225f..ec988e5 100644 --- a/ChessAndroid/app/src/main/cpp/chess/TranspositionTable.h +++ b/ChessAndroid/app/src/main/cpp/chess/TranspositionTable.h @@ -1,103 +1,99 @@ -#pragma once - -#include -#include -#include - -#include "Move.h" - -class SearchEntry -{ - friend class TranspositionTable; - -public: - enum class Bound : u8 - { - NONE = 0, - EXACT = 0b01, - ALPHA = 0b10, - BETA = 0b11 - }; - - SearchEntry() = default; - - constexpr SearchEntry(const u64 key, const int depth, const Move move, const bool qSearch, const Bound bound) - : _key16(key >> 48u), _moveFromTo(move.getFromToBits()), _value(move.getScore()), _depth8(depth), - generation((qSearch << 7u) | (u8(bound) << 5u)) {} - - constexpr u16 key() const noexcept { return _key16; } - - constexpr Move move() const noexcept { return Move{ _moveFromTo, i32(_value) }; } - - constexpr i32 depth() const noexcept { return i32(_depth8); } - - constexpr bool qSearch() const noexcept { return generation & QSEARCH_BITS; } - - constexpr Bound bound() const noexcept - { - return static_cast((generation & BOUND_BITS) >> 5u); - } - - constexpr u8 age() const noexcept { return generation & AGE_BITS; } - - constexpr void setAge(const u8 newAge) noexcept - { - generation = (generation & ~AGE_BITS) | (newAge & AGE_BITS); - } - -private: - u16 _key16{}; - u16 _moveFromTo{}; - i16 _value{}; - i8 _depth8{}; - - /** - * 0b0001'1111 - age - * 0b0110'0000 - Bound - * 0b1000'0000 - qSearch - */ - u8 generation{}; - - static constexpr u8 AGE_BITS = 0b0001'1111; - static constexpr u8 BOUND_BITS = 0b0110'0000; - static constexpr u8 QSEARCH_BITS = 0b1000'0000; -}; - -class TranspositionTable -{ - static constexpr usize MUTEX_COUNT = 512; - static constexpr usize CLUSTER_SIZE = 4; - - struct Cluster - { - std::array entries; - }; - - static_assert(sizeof(Cluster) == 32, "Wrong Cluster Size"); - -public: - explicit TranspositionTable(usize sizeMb); - - TranspositionTable(const TranspositionTable &) = delete; - TranspositionTable(TranspositionTable &&) = delete; - ~TranspositionTable(); - - TranspositionTable &operator=(const TranspositionTable &) = delete; - TranspositionTable &operator=(TranspositionTable &&) = delete; - - void prefetch(u64 zKey) const noexcept; - std::optional probe(u64 zKey) const noexcept; - - void insert(u64 zKey, SearchEntry entry) noexcept; - bool setSize(usize sizeMb); - void update() noexcept; - u8 currentAge() const noexcept; - void clear() noexcept; - -private: - usize _size{}; - u64 _hashMask{}; - u8 _currentAge{}; - Cluster *_clusters = nullptr; - mutable std::array _mutexes{}; -}; +#pragma once + +#include +#include +#include + +#include "Move.h" + +class SearchEntry +{ + friend class TranspositionTable; + +public: + enum class Bound : u8 + { + NONE = 0, + EXACT = 0b01, + ALPHA = 0b10, + BETA = 0b11 + }; + + SearchEntry() = default; + + constexpr SearchEntry(const u64 key, const int depth, const Move move, const bool qSearch, const Bound bound) + : _key16(key >> 48u), _moveFromTo(move.getFromToBits()), _value(move.getScore()), _depth8(depth) + { + _field.setAs<1>(bound); + _field.setAs<2>(qSearch); + } + + constexpr u16 key() const noexcept { return _key16; } + + constexpr Move move() const noexcept { return Move{ _moveFromTo, i32(_value) }; } + + constexpr i32 depth() const noexcept { return i32(_depth8); } + + constexpr u8 age() const noexcept { return _field.get<0>(); } + + constexpr Bound bound() const noexcept { return _field.getAs<1, Bound>(); } + + constexpr bool qSearch() const noexcept { return _field.getAs<2, bool>(); } + + constexpr void setAge(const u8 newAge) noexcept + { + _field.set<0>(newAge); + } + +private: + u16 _key16{}; + u16 _moveFromTo{}; + i16 _value{}; + i8 _depth8{}; + + /** + * 0 - age + * 1 - Bound + * 2 - qSearch + */ + Bitfield _field{}; + + static constexpr auto AGE_MASK = (1u << 5u) - 1u; +}; + +class TranspositionTable +{ + static constexpr usize CLUSTER_SIZE = 4; + + struct Cluster + { + std::array entries; + }; + + static_assert(sizeof(Cluster) == 32, "Wrong Cluster Size"); + +public: + explicit TranspositionTable(usize sizeMb); + + TranspositionTable(const TranspositionTable &) = delete; + TranspositionTable(TranspositionTable &&) = delete; + ~TranspositionTable(); + + TranspositionTable &operator=(const TranspositionTable &) = delete; + TranspositionTable &operator=(TranspositionTable &&) = delete; + + void prefetch(u64 zKey) const noexcept; + std::optional probe(u64 zKey) const noexcept; + + void insert(u64 zKey, SearchEntry entry) noexcept; + bool setSize(usize sizeMb); + void update() noexcept; + u8 currentAge() const noexcept; + void clear() noexcept; + +private: + usize _size{}; + u64 _hashMask{}; + u8 _currentAge{}; + Cluster *_clusters = nullptr; +}; diff --git a/ChessAndroid/app/src/main/cpp/chess/Uci.cpp b/ChessAndroid/app/src/main/cpp/chess/Uci.cpp index 4480ff0..f8e77d8 100644 --- a/ChessAndroid/app/src/main/cpp/chess/Uci.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/Uci.cpp @@ -56,7 +56,8 @@ void Uci::loop() else if (token == "stop") { Search::stopSearch(); - _searchThread.join(); + if (_searchThread.joinable()) + _searchThread.join(); std::cout << "Joined Thread\n"; } else if (token == "quit") quit = true; @@ -107,16 +108,22 @@ void Uci::loop() const auto results = Tests::runEvaluationTests(); if (results.empty()) std::cout << "Test Completed Successfully\n"; - else - std::cout << results; - } else if (token == "perft") - Tests::runPerftTests(); - else - std::cout << "Unknown command\n"; - - std::cout.flush(); - - if (quit) + else + std::cout << results; + } else if (token == "perft") + { + i32 depth{}; + is >> depth; + if (depth > 0) + Tests::runPerftForPosition(_board.getFen(), depth); + else + Tests::runPerftTests(); + + } + + std::cout.flush(); + + if (quit) break; line.clear(); @@ -139,7 +146,7 @@ void Uci::setOption(std::istringstream &is) { usize hashSize{}; is >> hashSize; - _hashSizeMb = std::clamp(hashSize, 4u, 2048u); + _hashSizeMb = std::clamp(hashSize, 4u, 4096u); std::cout << "Hash Size has been set to " << _hashSizeMb << "MB" << std::endl; } else if (token == "bookpath") diff --git a/ChessAndroid/app/src/main/cpp/chess/algorithm/Evaluation.cpp b/ChessAndroid/app/src/main/cpp/chess/algorithm/Evaluation.cpp index 5ecd4aa..5a40d25 100644 --- a/ChessAndroid/app/src/main/cpp/chess/algorithm/Evaluation.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/algorithm/Evaluation.cpp @@ -7,7 +7,6 @@ #include "../Stats.h" #include "../Psqt.h" #include "../PawnStructureTable.h" -#include "../Score.h" namespace { @@ -358,13 +357,9 @@ Score Eval::evaluatePieces() noexcept // Don't use the Pawn Structure Table if we are Tracing the Eval if (Trace || pawnsEntry.pawns != pawns) { - constexpr Piece piece{ PAWN, Us }; - - for (u8 pieceNumber{}; pieceNumber < board.pieceCount[piece]; ++pieceNumber) - { - const auto square = toSquare(board.pieceList[piece][pieceNumber]); - pawnScore += evaluatePawn(square); - } + Bitboard bb = board.getPieces(PAWN, Us); + while (bb.notEmpty()) + pawnScore += evaluatePawn(bb.popLsb()); PawnTable.insert({ pawns, pawnScore }); } else @@ -387,10 +382,10 @@ Score Eval::evaluatePieces() noexcept _mobilityArea[Us] = ~board.getPieces(Us) & ~Attacks::pawnAttacks(board.getPieces(PAWN, Them)); - Piece piece{ KNIGHT, Us }; - for (u8 pieceNumber{}; pieceNumber < board.pieceCount[piece]; ++pieceNumber) + Bitboard pieces = board.getPieces(KNIGHT, Us); + while (pieces.notEmpty()) { - const Square square = toSquare(board.pieceList[piece][pieceNumber]); + const Square square = pieces.popLsb(); const auto knightScore = evaluateKnight(square); knightBishopBonus(square); @@ -403,10 +398,10 @@ Score Eval::evaluatePieces() noexcept updateKingAttacks(KNIGHT, attacks); } - piece = { BISHOP, Us }; - for (u8 pieceNumber{}; pieceNumber < board.pieceCount[piece]; ++pieceNumber) + pieces = board.getPieces(BISHOP, Us); + while (pieces.notEmpty()) { - const auto square = toSquare(board.pieceList[piece][pieceNumber]); + const Square square = pieces.popLsb(); const auto bishopScore = evaluateBishop(square); knightBishopBonus(square); @@ -419,13 +414,13 @@ Score Eval::evaluatePieces() noexcept updateKingAttacks(BISHOP, attacks); } - if (board.pieceCount[piece] >= 2) + if (board.pieceCount[Piece{ BISHOP, Us }] >= 2) score += 40; - piece = { ROOK, Us }; - for (u8 pieceNumber{}; pieceNumber < board.pieceCount[piece]; ++pieceNumber) + pieces = board.getPieces(ROOK, Us); + while (pieces.notEmpty()) { - const auto square = toSquare(board.pieceList[piece][pieceNumber]); + const Square square = pieces.popLsb(); const auto rookScore = evaluateRook(square); if constexpr (Trace) @@ -437,10 +432,10 @@ Score Eval::evaluatePieces() noexcept updateKingAttacks(ROOK, attacks); } - piece = { QUEEN, Us }; - for (u8 pieceNumber{}; pieceNumber < board.pieceCount[piece]; ++pieceNumber) + pieces = board.getPieces(QUEEN, Us); + while (pieces.notEmpty()) { - const auto square = toSquare(board.pieceList[piece][pieceNumber]); + const Square square = pieces.popLsb(); const auto queenScore = evaluateQueen(square); if constexpr (Trace) @@ -531,7 +526,7 @@ Score Eval::evaluateAttacks() const noexcept (board.pieceCount[Piece{ QUEEN, Them }] + board.pieceCount[Piece{ QUEEN, Us }]) == 1; - const auto sq = toSquare(board.pieceList[Piece{ QUEEN, Them }][0]); + const auto sq = board.getPieces(QUEEN, Them).bitScanForward(); const auto safeSpots = _mobilityArea[Us] & ~stronglyProtected & ~board.getPieces(PAWN, Them); @@ -544,7 +539,7 @@ Score Eval::evaluateAttacks() const noexcept QUEEN_THREAT_BY_KNIGHT * (knightAttacks & safeSpots).count(); const auto sliderAttacksScore = QUEEN_THREAT_BY_SLIDER * - (sliderAttacks & safeSpots & _attacksMultiple[Us]).count() * + (sliderAttacks & safeSpots & _attacksMultiple[Us]).count() * (1 + imbalance); totalValue += knightAttacksScore + sliderAttacksScore; @@ -557,7 +552,7 @@ Score Eval::evaluateAttacks() const noexcept const auto safe = ~_allAttacks[Them] | _allAttacks[Us]; const auto safePawnsAttacks = - Attacks::pawnAttacks(board.getPieces(PAWN, Us) & safe) & nonPawnEnemies; + Attacks::pawnAttacks(board.getPieces(PAWN, Us) & safe) & nonPawnEnemies; const auto safePawnThreatScore = THREAT_BY_SAFE_PAWN * safePawnsAttacks.count(); totalValue += safePawnThreatScore; @@ -651,7 +646,7 @@ Score Eval::evaluateKnight(const Square square) const noexcept && (attacks & targets).empty() // no relevant attacks && (!(targets & ((bb & QUEEN_SIDE).notEmpty() ? QUEEN_SIDE : KING_SIDE)).several())) value += UNCONTESTED_OUTPOST * - (pawns & ((bb & QUEEN_SIDE).notEmpty() ? QUEEN_SIDE : KING_SIDE)).count(); + (pawns & ((bb & QUEEN_SIDE).notEmpty() ? QUEEN_SIDE : KING_SIDE)).count(); } const int mobility = (attacks & _mobilityArea[Us]).count(); @@ -695,11 +690,11 @@ Score Eval::evaluateBishop(const Square square) const noexcept // Enemy pawns x-rayed value -= BISHOP_XRAY_PAWNS * - (Attacks::bishopXRayAttacks(square) & board.getPieces(PAWN, Them)).count(); + (Attacks::bishopXRayAttacks(square) & board.getPieces(PAWN, Them)).count(); // Long Diagonal Bishop const auto centerAttacks = - Attacks::bishopAttacks(square, board.getPieces(PAWN, Them)) & CENTER_SQUARES; + Attacks::bishopAttacks(square, board.getPieces(PAWN, Them)) & CENTER_SQUARES; if (centerAttacks.several()) value.mg += 45; @@ -784,7 +779,7 @@ Score Eval::evaluateKing() const noexcept } value += KING_PAWN_SHIELD * - (MASK_PAWN_SHIELD[Us][square] & board.getPieces(PAWN, Us)).count(); + (MASK_PAWN_SHIELD[Us][square] & board.getPieces(PAWN, Us)).count(); return value; } diff --git a/ChessAndroid/app/src/main/cpp/chess/algorithm/Search.cpp b/ChessAndroid/app/src/main/cpp/chess/algorithm/Search.cpp index 0779dd9..4533bd8 100644 --- a/ChessAndroid/app/src/main/cpp/chess/algorithm/Search.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/algorithm/Search.cpp @@ -59,13 +59,13 @@ Move Search::findBestMove(Board board, const SearchOptions &searchOptions) board.ply = 0; // Reset Depth Counter _sharedState.reset(); - - Stats::restartTimer(); - - if (PolyBook::initialized() && _sharedState.useBook) - { - const Move move = PolyBook::getBookMove(board); - if (!move.empty()) + + Stats::restartTimer(); + + if (PolyBook::isEnabled() && _sharedState.useBook) + { + const Move move = PolyBook::getBookMove(board); + if (!move.empty()) { std::cout << "bestmove " << move.toString() << std::endl; return move; diff --git a/ChessAndroid/app/src/main/cpp/chess/algorithm/Search.h b/ChessAndroid/app/src/main/cpp/chess/algorithm/Search.h index 85aad1a..a8a7b6e 100644 --- a/ChessAndroid/app/src/main/cpp/chess/algorithm/Search.h +++ b/ChessAndroid/app/src/main/cpp/chess/algorithm/Search.h @@ -1,76 +1,76 @@ -#pragma once - -#include "../SearchOptions.h" -#include "../Move.h" -#include "../TranspositionTable.h" - -class Board; - -class Search final -{ -private: - struct SharedState - { - bool stopped{}; - std::atomic_uint64_t nodes{}; - - mutable std::mutex mutex{}; - // Stats for the last time the depth was updated - std::atomic_int depth{}; - int bestScore{}; - i64 time{}; - - // This should only be read and written by the main thread - int lastReportedDepth{}; - Move lastReportedBestMove{}; - bool useBook{}; - - void reset() noexcept - { - stopped = false; - nodes = 0; - depth = 0; - bestScore = VALUE_MIN; - time = 0; - lastReportedDepth = 0; - } - - void fullReset() noexcept - { - reset(); - useBook = true; - } - }; - - static SearchOptions _searchOptions; - static TranspositionTable _transpositionTable; - static SharedState _sharedState; - -public: - Search() = delete; - Search(const Search &) = delete; - Search(Search &&) = delete; - - Search &operator=(const Search &) = delete; - Search &operator=(Search &&) = delete; - - static void clearAll(); - static void stopSearch(); - static bool setTableSize(usize sizeMb); - - static Move findBestMove(Board board, const SearchOptions &searchOptions); - - static auto &getTranspTable() noexcept { return _transpositionTable; } - -private: - static void printUci(Board &board); - static void iterativeDeepening(Board board, int targetDepth); - static int aspirationWindow(Board &board, int depth, int bestScore); - static int search(Board &board, int alpha, int beta, int depth, bool isPvNode, - bool doNull, bool doLmr); - static int searchCaptures(Board &board, int alpha, int beta, int depth); - - inline static void storeTTEntry(const Move &bestMove, u64 key, int alpha, int originalAlpha, - int beta, int depth, bool qSearch); - static bool checkTimeAndStop(); -}; +#pragma once + +#include "../SearchOptions.h" +#include "../Move.h" +#include "../TranspositionTable.h" + +class Board; + +class Search final +{ +private: + struct SharedState + { + bool stopped{}; + std::atomic_uint64_t nodes{}; + + mutable std::mutex mutex{}; + // Stats for the last time the depth was updated + std::atomic_int depth{}; + int bestScore{}; + i64 time{}; + + // This should only be read and written by the main thread + int lastReportedDepth{}; + Move lastReportedBestMove{}; + bool useBook{}; + + void reset() noexcept + { + stopped = false; + nodes = 0; + depth = 0; + bestScore = VALUE_MIN; + time = 0; + lastReportedDepth = 0; + } + + void fullReset() noexcept + { + reset(); + useBook = true; + } + }; + + static SearchOptions _searchOptions; + static TranspositionTable _transpositionTable; + static SharedState _sharedState; + +public: + Search() = delete; + Search(const Search &) = delete; + Search(Search &&) = delete; + + Search &operator=(const Search &) = delete; + Search &operator=(Search &&) = delete; + + static void clearAll(); + static void stopSearch(); + static bool setTableSize(usize sizeMb); + + static Move findBestMove(Board board, const SearchOptions &searchOptions); + + static auto &getTranspTable() noexcept { return _transpositionTable; } + +private: + static void printUci(Board &board); + static void iterativeDeepening(Board board, int targetDepth); + static int aspirationWindow(Board &board, int depth, int bestScore); + static int search(Board &board, int alpha, int beta, int depth, bool isPvNode, + bool doNull, bool doLmr); + static int searchCaptures(Board &board, int alpha, int beta, int depth); + + inline static void storeTTEntry(const Move &bestMove, u64 key, int alpha, int originalAlpha, + int beta, int depth, bool qSearch); + static bool checkTimeAndStop(); +}; diff --git a/ChessAndroid/app/src/main/cpp/chess/persistence/FenParser.cpp b/ChessAndroid/app/src/main/cpp/chess/persistence/FenParser.cpp index 5e2be4d..e1590ab 100644 --- a/ChessAndroid/app/src/main/cpp/chess/persistence/FenParser.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/persistence/FenParser.cpp @@ -43,7 +43,7 @@ bool FenParser::parseFen(Board &board, const std::string &fen) std::string ep = "-"; stream >> ep; auto &enPassantSq = board.state.enPassantSq; - enPassantSq = SQUARE_NB; + enPassantSq = SQ_NONE; if (ep != "-") { if (ep.length() != 2) @@ -52,7 +52,7 @@ bool FenParser::parseFen(Board &board, const std::string &fen) return false; const Square sq = ::toSquare(int(ep[0] - 'a'), int(ep[1] - '1')); - enPassantSq = sq < SQUARE_NB ? sq : SQUARE_NB; + enPassantSq = sq < SQ_NONE ? sq : SQ_NONE; } // HalfMove Clock diff --git a/ChessAndroid/app/src/main/cpp/chess/polyglot/PolyBook.cpp b/ChessAndroid/app/src/main/cpp/chess/polyglot/PolyBook.cpp index 3f7fcc3..ab671c6 100644 --- a/ChessAndroid/app/src/main/cpp/chess/polyglot/PolyBook.cpp +++ b/ChessAndroid/app/src/main/cpp/chess/polyglot/PolyBook.cpp @@ -207,12 +207,9 @@ namespace PolyKeys u64(0xF8D626AAAF278509), }; - enum - { - CASTLE_OFFSET = 768, - EN_PASSANT_OFFSET = 772, - TURN_OFFSET = 780, - }; + static constexpr usize CastleOffset = 768; + static constexpr usize EnPassantOffset = 772; + static constexpr usize TurnOffset = 780; } namespace PolyBook @@ -240,13 +237,14 @@ namespace PolyBook const auto enPass = Bitboard::fromSquare(board.getEnPassantSq()); if (board.colorToMove == WHITE) - return !(Attacks::pawnAttacks(board.getPieces(PAWN, WHITE)) & enPass).empty(); + return (Attacks::pawnAttacks(board.getPieces(PAWN, WHITE)) & enPass).notEmpty(); else - return !(Attacks::pawnAttacks(board.getPieces(PAWN, BLACK)) & enPass).empty(); + return (Attacks::pawnAttacks(board.getPieces(PAWN, BLACK)) & enPass).notEmpty(); } static std::optional bookPath; - static std::vector vecBook; + static std::vector bookVector; + static bool enabled = false; void initBook(const std::string &path) { @@ -266,28 +264,32 @@ namespace PolyBook const auto streamSize = static_cast(bookFile.tellg()); const auto entriesCount = streamSize / sizeof(BookMove); - vecBook = std::vector(entriesCount); + bookVector = std::vector(entriesCount); std::cout << "Opening Book contains " << entriesCount << " entries\n"; bookFile.seekg(0, std::ios::beg); - if (!bookFile.read(reinterpret_cast(vecBook.data()), + if (!bookFile.read(reinterpret_cast(bookVector.data()), entriesCount * sizeof(BookMove))) { std::cout << "Error: Failed to Read File!\n"; } } - bool initialized() noexcept { return bookPath.has_value(); } + bool isInitialized() noexcept { return bookPath.has_value(); } + + void enable(const bool enable) noexcept { enabled = enable; } + + bool isEnabled() noexcept { return isInitialized() && enabled; } void clearBook() { if (bookPath.has_value()) bookPath = std::nullopt; - if (!vecBook.empty()) + if (!bookVector.empty()) { - vecBook.clear(); - vecBook.shrink_to_fit(); + bookVector.clear(); + bookVector.shrink_to_fit(); } } @@ -310,24 +312,24 @@ namespace PolyBook // Castling if (board.canCastleKs()) - result ^= PolyKeys::Random64[PolyKeys::CASTLE_OFFSET + 0]; + result ^= PolyKeys::Random64[PolyKeys::CastleOffset + 0]; if (board.canCastleQs()) - result ^= PolyKeys::Random64[PolyKeys::CASTLE_OFFSET + 1]; + result ^= PolyKeys::Random64[PolyKeys::CastleOffset + 1]; if (board.canCastleKs()) - result ^= PolyKeys::Random64[PolyKeys::CASTLE_OFFSET + 2]; + result ^= PolyKeys::Random64[PolyKeys::CastleOffset + 2]; if (board.canCastleQs()) - result ^= PolyKeys::Random64[PolyKeys::CASTLE_OFFSET + 3]; + result ^= PolyKeys::Random64[PolyKeys::CastleOffset + 3]; // En Passant if (board.getEnPassantSq() != SQ_NONE && hasEnPassPawnForCapture(board)) { - result ^= PolyKeys::Random64[PolyKeys::EN_PASSANT_OFFSET + fileOf(board.getEnPassantSq())]; + result ^= PolyKeys::Random64[PolyKeys::EnPassantOffset + fileOf(board.getEnPassantSq())]; } // SideKey if (board.colorToMove == WHITE) { - result ^= PolyKeys::Random64[PolyKeys::TURN_OFFSET]; + result ^= PolyKeys::Random64[PolyKeys::TurnOffset]; } // Generate the key and swap it to big endian using flipVertical @@ -380,7 +382,7 @@ namespace PolyBook std::array foundMoves{}; usize foundMovesCount{}; - for (const BookMove &bookMove : vecBook) + for (const BookMove &bookMove : bookVector) { if (bookMove.key == polyKey) { @@ -392,10 +394,17 @@ namespace PolyBook if (foundMovesCount == foundMoves.max_size()) break; + } else if (foundMovesCount != 0) + { + /* + * If we have found any moves but the current one does not belong to this board, + * we can stop searching since the file is sorted + */ + break; } } - // TODO check if these are valid moves + // TODO check if these are valid moves? if (foundMovesCount == 0) return {}; diff --git a/ChessAndroid/app/src/main/cpp/chess/polyglot/PolyBook.h b/ChessAndroid/app/src/main/cpp/chess/polyglot/PolyBook.h index 3e9710d..0666816 100644 --- a/ChessAndroid/app/src/main/cpp/chess/polyglot/PolyBook.h +++ b/ChessAndroid/app/src/main/cpp/chess/polyglot/PolyBook.h @@ -2,12 +2,14 @@ #include "../Board.h" -namespace PolyBook -{ - void initBook(const std::string &bookPath); - bool initialized() noexcept; - void clearBook(); - - u64 getKeyFromBoard(const Board &board) noexcept; +namespace PolyBook +{ + void initBook(const std::string &bookPath); + bool isInitialized() noexcept; + void enable(bool enable) noexcept; + bool isEnabled() noexcept; + void clearBook(); + + u64 getKeyFromBoard(const Board &board) noexcept; Move getBookMove(const Board &board) noexcept; } diff --git a/ChessAndroid/app/src/main/ic_launcher-playstore.png b/ChessAndroid/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..acf7eff Binary files /dev/null and b/ChessAndroid/app/src/main/ic_launcher-playstore.png differ diff --git a/ChessAndroid/app/src/main/ic_launcher-web.png b/ChessAndroid/app/src/main/ic_launcher-web.png deleted file mode 100644 index 552a89f..0000000 Binary files a/ChessAndroid/app/src/main/ic_launcher-web.png and /dev/null differ diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ChessApp.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ChessApp.kt index 4a6542a..5571302 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ChessApp.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ChessApp.kt @@ -1,16 +1,22 @@ package net.theluckycoder.chess import android.app.Application +import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import net.theluckycoder.chess.cpp.Native +import net.theluckycoder.chess.model.SearchOptions import net.theluckycoder.chess.utils.SettingsDataStore import java.io.File +import kotlin.time.ExperimentalTime +import kotlin.time.seconds @Suppress("unused") class ChessApp : Application() { + @OptIn(ExperimentalTime::class) override fun onCreate() { super.onCreate() @@ -26,7 +32,8 @@ class ChessApp : Application() { launch { if (dataStore.firstStart().first()) { // Set the default Engine Settings from native code - val engineSettings = Native.getSearchOptions() + val engineSettings = SearchOptions.getNativeSearchOptions() + .copy(searchTime = SettingsDataStore.DEFAULT_SEARCH_TIME.seconds) dataStore.setEngineSettings(engineSettings) dataStore.setFirstStart(false) } @@ -35,10 +42,9 @@ class ChessApp : Application() { } private fun copyBook() { - val bookName = "Book.bin" - val dest = File(filesDir, bookName) + val dest = getBookPath(this) - assets.open(bookName).use { input -> + assets.open(BOOK_NAME).use { input -> dest.outputStream().use { output -> input.copyTo(output) } @@ -51,5 +57,9 @@ class ChessApp : Application() { init { System.loadLibrary("chess") } + + const val BOOK_NAME = "OpeningBook.bin" + + fun getBookPath(context: Context): File = File(context.filesDir, BOOK_NAME) } } diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/BoardChangeListener.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/BoardChangeListener.kt similarity index 72% rename from ChessAndroid/app/src/main/java/net/theluckycoder/chess/BoardChangeListener.kt rename to ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/BoardChangeListener.kt index a5de49f..ac8400c 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/BoardChangeListener.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/BoardChangeListener.kt @@ -1,10 +1,9 @@ -package net.theluckycoder.chess +package net.theluckycoder.chess.cpp import androidx.annotation.Keep interface BoardChangeListener { - @Suppress("unused") @Keep // Called by native code fun boardChanged(gameStateInt: Int) } diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/Native.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/Native.kt similarity index 52% rename from ChessAndroid/app/src/main/java/net/theluckycoder/chess/Native.kt rename to ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/Native.kt index 92bd543..33ccb58 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/Native.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/Native.kt @@ -1,60 +1,36 @@ -package net.theluckycoder.chess - -import net.theluckycoder.chess.model.IndexedPiece -import net.theluckycoder.chess.model.Move -import net.theluckycoder.chess.model.SearchOptions -import kotlin.time.ExperimentalTime - -object Native { - - external fun initBoard(boardChangeListener: BoardChangeListener, isPlayerWhite: Boolean) - external fun initBook(bookPath: String) - - external fun loadFen(playerWhite: Boolean, fen: String): Boolean - external fun loadFenMoves(playerWhite: Boolean, fen: String, moves: IntArray) - - external fun isEngineWorking(): Boolean - external fun isPlayerWhite(): Boolean - external fun isPlayersTurn(): Boolean - - external fun getPieces(): Array - external fun getPossibleMoves(square: Byte): Array - - fun makeMove(move: Move) = makeMove(move.content) - external fun makeMove(move: Int) - external fun makeEngineMove() - - external fun stopSearch() - - external fun getSearchOptions(): SearchOptions - - @OptIn(ExperimentalTime::class) - fun setSearchOptions(options: SearchOptions) = setSearchOptions( - options.searchDepth, - options.quietSearch, - options.threadCount, - options.hashSize, - options.searchTime.toLongMilliseconds(), - ) - - private external fun setSearchOptions( - searchDepth: Int, quietSearch: Boolean, - threadCount: Int, hashSizeMb: Int, - searchTime: Long, - ) - - external fun undoMoves() - external fun redoMoves() - - external fun getCurrentFen(): String - external fun getStartFen(): String - external fun getMovesHistory(): Array - external fun getCurrentMoveIndex(): Int - - // region Tests - - external fun perftTests() - external fun evaluationTests(): String? - - // endregion Tests -} +package net.theluckycoder.chess.cpp + +import net.theluckycoder.chess.model.IndexedPiece +import net.theluckycoder.chess.model.Move + +object Native { + + external fun initBoard(boardChangeListener: BoardChangeListener, isPlayerWhite: Boolean) + external fun initBook(bookPath: String) + external fun enableBook(enable: Boolean) + external fun setSearchListener(searchListener: SearchListener) + + external fun loadFen(playerWhite: Boolean, fen: String): Boolean + external fun loadFenMoves(playerWhite: Boolean, fen: String, moves: IntArray) + + external fun isEngineBusy(): Boolean + external fun isPlayerWhite(): Boolean + external fun isPlayersTurn(): Boolean + + external fun getPieces(): Array + external fun getPossibleMoves(square: Byte): Array + + fun makeMove(move: Move) = makeMove(move.content) + private external fun makeMove(move: Int) + external fun makeEngineMove() + + external fun stopSearch() + + external fun undoMoves() + external fun redoMoves() + + external fun getCurrentFen(): String + external fun getStartFen(): String + external fun getMovesHistory(): Array + external fun getCurrentMoveIndex(): Int +} diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/SearchListener.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/SearchListener.kt new file mode 100644 index 0000000..a2520ed --- /dev/null +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/SearchListener.kt @@ -0,0 +1,9 @@ +package net.theluckycoder.chess.cpp + +import androidx.annotation.Keep + +interface SearchListener { + + @Keep // Called by native code + fun onFinish(success: Boolean) +} diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/Tests.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/Tests.kt new file mode 100644 index 0000000..0fbcc0b --- /dev/null +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/cpp/Tests.kt @@ -0,0 +1,7 @@ +package net.theluckycoder.chess.cpp + +class Tests { + external fun perftTests() + + external fun evaluationTests(): String? +} \ No newline at end of file diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/model/SearchOptions.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/model/SearchOptions.kt index 9c74e89..4fa2529 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/model/SearchOptions.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/model/SearchOptions.kt @@ -22,4 +22,26 @@ data class SearchOptions( hashSize: Int, quietSearch: Boolean, ) : this(searchDepth, threadCount, searchTime.milliseconds, hashSize, quietSearch) + + companion object { + + @JvmStatic + external fun getNativeSearchOptions(): SearchOptions + + @OptIn(ExperimentalTime::class) + fun setNativeSearchOptions(options: SearchOptions) = setNativeSearchOptions( + options.searchDepth, + options.quietSearch, + options.threadCount, + options.hashSize, + options.searchTime.toLongMilliseconds(), + ) + + @JvmStatic + private external fun setNativeSearchOptions( + searchDepth: Int, quietSearch: Boolean, + threadCount: Int, hashSizeMb: Int, + searchTime: Long, + ) + } } diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/model/Tile.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/model/Tile.kt index ba7fe68..6b34a75 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/model/Tile.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/model/Tile.kt @@ -11,4 +11,4 @@ data class Tile( object Selected : State() object Moved : State() } -} \ No newline at end of file +} diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/Board.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/ChessBoard.kt similarity index 92% rename from ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/Board.kt rename to ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/ChessBoard.kt index 30f8c94..8155c39 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/Board.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/ChessBoard.kt @@ -1,4 +1,4 @@ -package net.theluckycoder.chess.ui.home +package net.theluckycoder.chess.ui import android.app.Application import androidx.compose.animation.core.animateOffsetAsState @@ -26,13 +26,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import net.theluckycoder.chess.Native import net.theluckycoder.chess.R +import net.theluckycoder.chess.cpp.Native import net.theluckycoder.chess.model.* -import net.theluckycoder.chess.ui.AlertDialogTitle import net.theluckycoder.chess.utils.SettingsDataStore -import net.theluckycoder.chess.viewmodel.HomeViewModel import kotlin.math.min private val PIECES_RESOURCES = intArrayOf( @@ -55,6 +52,7 @@ fun ChessBoard( tiles: List, pieces: List, gameState: GameState, + onPieceClick: (piece: Piece) -> Unit, ) = BoxWithConstraints( modifier = modifier ) { @@ -71,7 +69,7 @@ fun ChessBoard( BoardTiles(boardSize, tileSize, isPlayerWhite, tiles, showPossibleMoves) - BoardPieces(tileSize, isPlayerWhite, pieces, gameState) + BoardPieces(tileSize, isPlayerWhite, pieces, gameState, onPieceClick) if (showCoordinates) { BoardCoordinates(tileSize) @@ -95,7 +93,7 @@ private fun BoardTiles( val movedTileColor = colorResource(id = R.color.tile_last_moved) val possibleTileCircleSize = with(currentDensity) { 8.dp.toPx() } - val tilePx = with(currentDensity) { tileDp.toPx() } + val tilePx = with(LocalDensity.current) { tileDp.toPx() } val tileSize = Size(tilePx, tilePx) val possibleCapturePath = remember(tilePx) { @@ -197,7 +195,7 @@ private fun BoardPieces( isPlayerWhite: Boolean, pieces: List, gameState: GameState, - viewModel: HomeViewModel = viewModel() + onPieceClick: (piece: Piece) -> Unit, ) { val whiteInCheck = gameState == GameState.WHITE_IN_CHECK val blackInCheck = gameState == GameState.BLACK_IN_CHECK @@ -214,20 +212,21 @@ private fun BoardPieces( val animatedOffset by animateOffsetAsState(targetValue = offset) val (animatedX, animatedY) = with(LocalDensity.current) { animatedOffset.x.toDp() to animatedOffset.y.toDp() } - var backgroundModifier: Modifier = Modifier - - if (piece.type == Piece.KING) { - if ((whiteInCheck && piece.isWhite) || (blackInCheck && !piece.isWhite)) - backgroundModifier = Modifier.background(kingInCheckColor, CircleShape) - } + val backgroundModifier = if ( + (piece.type == Piece.KING) + && ((whiteInCheck && piece.isWhite) || (blackInCheck && !piece.isWhite)) + ) + Modifier.background(kingInCheckColor, CircleShape) + else + Modifier IconButton( modifier = Modifier - .requiredSize(tileDp) + .size(tileDp) .offset(animatedX, animatedY) .then(backgroundModifier), enabled = isPlayerWhite == piece.isWhite, - onClick = { viewModel.getPossibleMoves(piece.square) } + onClick = { onPieceClick(piece) } ) { Image(painter = getPieceDrawable(piece = piece), contentDescription = null) } diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/ChessComposables.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/ChessComposables.kt new file mode 100644 index 0000000..475fa9a --- /dev/null +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/ChessComposables.kt @@ -0,0 +1,203 @@ +package net.theluckycoder.chess.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconToggleButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.launch +import net.theluckycoder.chess.R +import net.theluckycoder.chess.model.Move +import net.theluckycoder.chess.model.Piece +import net.theluckycoder.chess.utils.CapturedPieces +import net.theluckycoder.chess.utils.isIndexVisible +import net.theluckycoder.chess.viewmodel.HomeViewModel + +@Composable +fun ChooseSidesToggle( + modifier: Modifier = Modifier, + sidesToggleIndex: MutableState, +) { + class Side(val painterRes: Int, val backgroundColorRes: Int, val contentDescriptionRes: Int) + + val sides = listOf( + Side(R.drawable.w_king, R.color.side_white, R.string.side_white), + Side(R.drawable.b_king, R.color.side_black, R.string.side_black), + Side(R.drawable.side_random, R.color.side_random, R.string.side_random), + ) + + Row( + modifier = modifier + .fillMaxWidth() + .padding(8.dp) + .clip(RoundedCornerShape(8.dp)) + ) { + sides.forEachIndexed { index, side -> + val backgroundColor = if (sidesToggleIndex.value == index) + MaterialTheme.colors.primary.copy(alpha = 0.5f) + else + colorResource(id = side.backgroundColorRes) + + IconToggleButton( + modifier = Modifier + .background(backgroundColor) + .weight(1f) + .padding(4.dp), + checked = sidesToggleIndex.value == index, + onCheckedChange = { sidesToggleIndex.value = index } + ) { + Icon( + modifier = Modifier.size(54.dp), + painter = painterResource(id = side.painterRes), + tint = Color.Unspecified, + contentDescription = stringResource(id = side.contentDescriptionRes), + ) + } + } + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun MovesHistory( + show: Boolean, + movesHistory: List, + currentMoveIndex: Int, +) = AnimatedVisibility( + visible = show, + enter = fadeIn() + expandIn(Alignment.TopCenter), + exit = shrinkOut(Alignment.TopCenter) + fadeOut(), + initiallyVisible = false, +) { + val listState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + + remember(movesHistory, currentMoveIndex) { + coroutineScope.launch { + if (movesHistory.isNotEmpty() + && currentMoveIndex in movesHistory.indices + && !listState.isIndexVisible(currentMoveIndex) + ) listState.animateScrollToItem(currentMoveIndex) + } + } + + LazyRow( + state = listState, + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF222222)) + .padding(4.dp), + content = { + if (movesHistory.isNotEmpty()) { + itemsIndexed(movesHistory) { index, item -> + val padding = if (index % 2 == 0) + Modifier.padding(start = 6.dp, end = 2.dp) + else + Modifier.padding(start = 2.dp, end = 6.dp) + + Row( + modifier = padding + ) { + if (index % 2 == 0) { + Text( + text = "${index / 2 + 1}. ", + color = Color.Gray, + fontSize = 13.sp, + ) + } + + val modifier = Modifier.padding(1.dp).then( + if (currentMoveIndex == index) + Modifier + .clip(RoundedCornerShape(2.dp)) + .background(Color.Gray) + else Modifier + ) + Text( + modifier = modifier, + text = item.toString(), + fontSize = 13.sp, + ) + } + } + } else + item { Text(modifier = Modifier.padding(1.dp), text = "", fontSize = 13.sp) } + } + ) +} + +@Composable +fun CapturedPiecesLists( + modifier: Modifier = Modifier, + show: Boolean, + content: @Composable ColumnScope.() -> Unit +) = Column(modifier) { + val viewModel = viewModel() + + val isPlayerWhite by viewModel.playerPlayingWhite.collectAsState() + val pieces by viewModel.pieces.collectAsState() + + val capturedPieces = remember(pieces.size) { CapturedPieces.from(pieces.map { it.toPiece() }) } + + // Top + if (show) { + CapturedPieceList( + if (isPlayerWhite) capturedPieces.capturedByWhite else capturedPieces.capturedByBlack, + if (isPlayerWhite) capturedPieces.blackScore else capturedPieces.whiteScore, + ) + } + + content() + + // Bottom + if (show) { + CapturedPieceList( + if (isPlayerWhite) capturedPieces.capturedByBlack else capturedPieces.capturedByWhite, + if (isPlayerWhite) capturedPieces.whiteScore else capturedPieces.blackScore + ) + } +} + +@Composable +private fun CapturedPieceList(pieces: List, score: Int) { + LazyRow(modifier = Modifier.fillMaxWidth().padding(4.dp).height(24.dp)) { + items(pieces) { piece -> + val id = when (piece) { + Piece.PAWN -> R.drawable.ic_pawn + Piece.KNIGHT -> R.drawable.ic_knight + Piece.BISHOP -> R.drawable.ic_bishop + Piece.ROOK -> R.drawable.ic_rook + Piece.QUEEN -> R.drawable.ic_queen + Piece.KING -> R.drawable.ic_king + else -> throw IllegalStateException("Unknown Piece") + } + + Icon( + painter = painterResource(id = id), + contentDescription = null + ) + } + + if (score != 0) { + item { Text(text = "+$score") } + } + } +} diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/Composables.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/Composables.kt index ac1fe6a..cec0b27 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/Composables.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/Composables.kt @@ -1,27 +1,9 @@ package net.theluckycoder.chess.ui -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.IconToggleButton -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import net.theluckycoder.chess.R @Composable fun AlertDialogTitle(text: String) { @@ -31,48 +13,3 @@ fun AlertDialogTitle(text: String) { fontWeight = FontWeight.Bold ) } - -@Composable -fun ChooseSidesToggle( - modifier: Modifier = Modifier, - sidesToggleIndex: MutableState, -) { - class Side(val painterRes: Int, val backgroundColorRes: Int, val contentDescriptionRes: Int) - - val sides = listOf( - Side(R.drawable.w_king, R.color.side_white, R.string.side_white), - Side(R.drawable.b_king, R.color.side_black, R.string.side_black), - Side(R.drawable.side_random, R.color.side_random, R.string.side_random), - ) - - Row( - modifier = modifier - .fillMaxWidth() - .padding(8.dp) - .clip(RoundedCornerShape(8.dp)) - ) { - sides.forEachIndexed { index, side -> - val backgroundColor = if (sidesToggleIndex.value == index) - MaterialTheme.colors.primary.copy(alpha = 0.5f) - else - colorResource(id = side.backgroundColorRes) - - IconToggleButton( - modifier = Modifier - .background(backgroundColor) - .weight(1f) - .padding(4.dp), - checked = sidesToggleIndex.value == index, - onCheckedChange = { sidesToggleIndex.value = index } - ) { - Icon( - modifier = Modifier.size(54.dp), - painter = painterResource(id = side.painterRes), - tint = Color.Unspecified, - contentDescription = stringResource(id = side.contentDescriptionRes), - ) - } - } - } -} - diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/home/HomeDialogs.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/home/HomeDialogs.kt index 990b25f..0427359 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/home/HomeDialogs.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/home/HomeDialogs.kt @@ -13,11 +13,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel -import net.theluckycoder.chess.Native +import net.theluckycoder.chess.cpp.Native import net.theluckycoder.chess.R import net.theluckycoder.chess.model.GameState import net.theluckycoder.chess.ui.AlertDialogTitle import net.theluckycoder.chess.ui.ChooseSidesToggle +import net.theluckycoder.chess.utils.Pgn import net.theluckycoder.chess.viewmodel.HomeViewModel import kotlin.math.roundToInt import kotlin.random.Random @@ -108,10 +109,18 @@ private fun NewGameDialog(viewModel: HomeViewModel = viewModel()) { @Composable private fun SharePositionDialog(viewModel: HomeViewModel = viewModel()) { val currentFen = remember { Native.getCurrentFen() } + val pgn = remember { + Pgn.export( + viewModel.playerPlayingWhite.value, + Native.getStartFen(), + viewModel.movesHistory.value, + viewModel.gameState.value + ) + } AlertDialog( onDismissRequest = { viewModel.showShareDialog.value = false }, - title = { AlertDialogTitle(text = stringResource(id = R.string.fen_position_share)) }, + title = { AlertDialogTitle(text = stringResource(id = R.string.fen_pgn_position_share)) }, text = { Column(Modifier.fillMaxWidth()) { Text( @@ -135,16 +144,31 @@ private fun SharePositionDialog(viewModel: HomeViewModel = viewModel()) { confirmButton = { val context = LocalContext.current val clipboardManager = LocalClipboardManager.current - TextButton( - onClick = { - clipboardManager.setText(AnnotatedString(currentFen)) - Toast.makeText(context, R.string.fen_position_copied, Toast.LENGTH_SHORT) - .show() - viewModel.showShareDialog.value = false + + Row { + TextButton( + onClick = { + clipboardManager.setText(AnnotatedString(currentFen)) + Toast.makeText(context, R.string.fen_position_copied, Toast.LENGTH_SHORT) + .show() + viewModel.showShareDialog.value = false + } + ) { + Text(text = stringResource(id = R.string.action_fen_copy)) + } + + TextButton( + onClick = { + clipboardManager.setText(AnnotatedString(pgn)) + Toast.makeText(context, R.string.pgn_position_copied, Toast.LENGTH_SHORT) + .show() + viewModel.showShareDialog.value = false + } + ) { + Text(text = stringResource(id = R.string.action_pgn_copy)) } - ) { - Text(text = stringResource(id = R.string.action_copy)) } + } ) } diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/home/HomeScreen.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/home/HomeScreen.kt index 6254463..735b793 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/home/HomeScreen.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/home/HomeScreen.kt @@ -2,20 +2,16 @@ package net.theluckycoder.chess.ui.home import android.content.Intent import android.content.res.Configuration -import androidx.compose.animation.* -import androidx.compose.foundation.background +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.expandIn +import androidx.compose.animation.shrinkOut import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.animatedVectorResource @@ -29,9 +25,11 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import net.theluckycoder.chess.Native import net.theluckycoder.chess.R -import net.theluckycoder.chess.model.Move +import net.theluckycoder.chess.cpp.Native +import net.theluckycoder.chess.ui.CapturedPiecesLists +import net.theluckycoder.chess.ui.ChessBoard +import net.theluckycoder.chess.ui.MovesHistory import net.theluckycoder.chess.ui.preferences.PreferencesActivity import net.theluckycoder.chess.viewmodel.HomeViewModel import kotlin.time.ExperimentalTime @@ -43,27 +41,32 @@ fun HomeScreen( val showMovesHistory by viewModel.dataStore.showMoveHistory().collectAsState(false) val movesHistory by viewModel.movesHistory.collectAsState() val currentMoveIndex by viewModel.currentMoveIndex.collectAsState() + val showCaptures by viewModel.dataStore.showCapturedPieces().collectAsState(false) + + HomeDialogs() when (LocalConfiguration.current.orientation) { Configuration.ORIENTATION_LANDSCAPE -> { Scaffold( modifier = Modifier.fillMaxSize(), - topBar = { - MovesHistory(showMovesHistory, movesHistory, currentMoveIndex) - }, ) { padding -> Row( Modifier .padding(padding) .fillMaxSize() ) { - BottomBar( - modifier = Modifier - .fillMaxHeight() - .weight(1f) - ) - HomeChessBoard() - HomeDialogs() + HomeChessBoard(viewModel) + + Column(modifier = Modifier.fillMaxHeight().weight(1f)) { + MovesHistory(showMovesHistory, movesHistory, currentMoveIndex) + + CapturedPiecesLists( + modifier = Modifier.fillMaxHeight(), + show = showCaptures + ) { + ActionsBar(Modifier.weight(1f).padding(8.dp)) + } + } } } } @@ -76,11 +79,15 @@ fun HomeScreen( MovesHistory(showMovesHistory, movesHistory, currentMoveIndex) } }, - bottomBar = { BottomBar() } + bottomBar = { ActionsBar() } ) { padding -> Box(Modifier.fillMaxSize().padding(padding)) { - HomeChessBoard(Modifier.align(Alignment.TopCenter)) - HomeDialogs() + CapturedPiecesLists( + modifier = Modifier.align(Alignment.TopCenter), + showCaptures + ) { + HomeChessBoard(viewModel) + } } } } @@ -89,7 +96,6 @@ fun HomeScreen( @Composable private fun HomeChessBoard( - modifier: Modifier = Modifier, viewModel: HomeViewModel = viewModel() ) { val isPlayerWhite by viewModel.playerPlayingWhite.collectAsState() @@ -98,11 +104,11 @@ private fun HomeChessBoard( val gameState by viewModel.gameState.collectAsState() ChessBoard( - modifier = modifier, isPlayerWhite = isPlayerWhite, tiles = tiles, pieces = pieces, - gameState = gameState + gameState = gameState, + onPieceClick = { viewModel.showPossibleMoves(it.square) } ) } @@ -120,7 +126,7 @@ private fun TopBar(viewModel: HomeViewModel = viewModel()) = TopAppBar( modifier = Modifier.padding(end = 16.dp) ) - val isEngineThinking by viewModel.isEngineThinking.collectAsState() + val isEngineThinking by viewModel.isEngineBusy.collectAsState() AnimatedVisibility( visible = isEngineThinking, @@ -148,73 +154,6 @@ private fun TopBar(viewModel: HomeViewModel = viewModel()) = TopAppBar( } ) -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun MovesHistory( - show: Boolean, - movesHistory: List, - currentMoveIndex: Int, -) = AnimatedVisibility( - visible = show, - enter = fadeIn() + expandIn(Alignment.TopCenter), - exit = shrinkOut(Alignment.TopCenter) + fadeOut(), - initiallyVisible = false, -) { - val listState = rememberLazyListState() - val coroutineScope = rememberCoroutineScope() - - remember(movesHistory, currentMoveIndex) { - coroutineScope.launch { - if (movesHistory.isNotEmpty() && currentMoveIndex in movesHistory.indices) - listState.animateScrollToItem(currentMoveIndex) - } - } - - LazyRow( - state = listState, - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF222222)) - .padding(4.dp), - content = { - if (movesHistory.isNotEmpty()) { - itemsIndexed(movesHistory) { index, item -> - val padding = if (index % 2 == 0) - Modifier.padding(start = 6.dp, end = 2.dp) - else - Modifier.padding(start = 2.dp, end = 6.dp) - - Row( - modifier = padding - ) { - if (index % 2 == 0) { - Text( - text = "${index / 2 + 1}. ", - color = Color.Gray, - fontSize = 13.sp, - ) - } - - val modifier = Modifier.padding(1.dp).then( - if (currentMoveIndex == index) - Modifier - .clip(RoundedCornerShape(2.dp)) - .background(Color.Gray) - else Modifier - ) - Text( - modifier = modifier, - text = item.toString(), - fontSize = 13.sp, - ) - } - } - } else - item { Text(modifier = Modifier.padding(1.dp), text = "", fontSize = 13.sp) } - } - ) -} - @Composable private fun AppBarActions(viewModel: HomeViewModel = viewModel()) { var showActionsMenu by remember { mutableStateOf(false) } @@ -235,7 +174,7 @@ private fun AppBarActions(viewModel: HomeViewModel = viewModel()) { viewModel.showImportDialog.value = true }) { Text(text = stringResource(id = R.string.fen_position_load)) } - val isEngineThinking by viewModel.isEngineThinking.collectAsState() + val isEngineThinking by viewModel.isEngineBusy.collectAsState() val basicDebug by viewModel.dataStore.showBasicDebug().collectAsState(false) if (basicDebug) { @@ -258,7 +197,7 @@ private fun AppBarActions(viewModel: HomeViewModel = viewModel()) { @OptIn(ExperimentalTime::class) @Composable -private fun BottomBar( +private fun ActionsBar( modifier: Modifier = Modifier, viewModel: HomeViewModel = viewModel() ) = Column( diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/preferences/PreferencesActivity.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/preferences/PreferencesActivity.kt index d023ae0..adc4274 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/preferences/PreferencesActivity.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/ui/preferences/PreferencesActivity.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp -import net.theluckycoder.chess.Native import net.theluckycoder.chess.R import net.theluckycoder.chess.ui.ChessMaterialTheme import net.theluckycoder.chess.utils.SettingsDataStore @@ -77,6 +76,13 @@ private fun getPreferenceItems( icon = painterResource(id = R.drawable.ic_pref_moves_history), defaultValue = SettingsDataStore.DEFAULT_SHOW_COORDINATES, ), + SwitchPreferenceItem( + title = stringResource(id = R.string.pref_captured_pieces), + summary = stringResource(id = R.string.pref_captured_pieces_desc), + prefKey = SettingsDataStore.SHOW_CAPTURED_PIECES, + icon = painterResource(id = R.drawable.ic_pawn), + defaultValue = SettingsDataStore.DEFAULT_SHOW_CAPTURED_PIECES, + ), SwitchPreferenceItem( title = stringResource(id = R.string.pref_piece_destinations), summary = stringResource(id = R.string.pref_piece_destinations_desc), @@ -126,6 +132,13 @@ private fun getPreferenceItems( valueRange = 1..Runtime.getRuntime().availableProcessors(), defaultValue = SettingsDataStore.DEFAULT_THREADS ), + SwitchPreferenceItem( + title = stringResource(id = R.string.pref_allow_opening_book), + summary = stringResource(id = R.string.pref_allow_opening_book_desc), + prefKey = SettingsDataStore.ALLOW_BOOK, + icon = painterResource(id = R.drawable.ic_pref_book), + defaultValue = true + ), SeekbarIntPreferenceItem( title = stringResource(id = R.string.pref_cache_size), summary = stringResource(id = R.string.pref_cache_size_desc), @@ -172,19 +185,7 @@ private fun getPreferenceItems( dependencyKey = SettingsDataStore.SHOW_DEBUG_BASIC, icon = painterResource(id = R.drawable.ic_pref_stats), defaultValue = false, - ), - EmptyPreferenceItem( - title = "Run Perft Test", - summary = "This is only meant for debugging", - dependencyKey = SettingsDataStore.SHOW_DEBUG_BASIC, - onClick = { Native.perftTests() } - ), - EmptyPreferenceItem( - title = "Run Evaluation Test", - summary = "This is only meant for debugging", - dependencyKey = SettingsDataStore.SHOW_DEBUG_BASIC, - onClick = { Native.evaluationTests() } - ), + ) ), ), ) diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/CapturedPieces.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/CapturedPieces.kt index 98af46b..dd26bca 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/CapturedPieces.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/CapturedPieces.kt @@ -3,32 +3,41 @@ package net.theluckycoder.chess.utils import net.theluckycoder.chess.model.Piece class CapturedPieces( - pieces: List, + val whiteScore: Int, + val blackScore: Int, + val capturedByWhite: List, + val capturedByBlack: List, ) { - private val piecesPartition = pieces.partition { it.isWhite } - private val whitePieces = piecesPartition.first - private val blackPieces = piecesPartition.second + companion object { + fun from(pieces: List): CapturedPieces { + val (whitePieces, blackPieces) = pieces.partition { it.isWhite } - private val absoluteWhiteScore = whitePieces.sumBy { it.score } - private val absoluteBlackScore = blackPieces.sumBy { it.score } + val totalWhiteScore = whitePieces.sumBy { it.score } + val totalBlackScore = blackPieces.sumBy { it.score } - fun getDifference(): Pair, List> { - val white = whitePieces.toMutableList() - val black = blackPieces.toMutableList() + val whiteScore = (totalWhiteScore - totalBlackScore).coerceAtLeast(0) + val blackScore = (totalBlackScore - totalWhiteScore).coerceAtLeast(0) - for (element in whitePieces) { - val index = black.indexOf(element) + val remainingWhite = whitePieces.map { it.type }.toMutableList() + val remainingBlack = blackPieces.map { it.type }.toMutableList() - if (index != -1) { - white.remove(element) - black.removeAt(index) + for (whitePiece in whitePieces) { + if (remainingBlack.contains(whitePiece.type)) { + remainingWhite.remove(whitePiece.type) + remainingBlack.remove(whitePiece.type) + } } - } - white.sortByDescending { it.type } - black.sortByDescending { it.type } + remainingWhite.sortByDescending { it } + remainingBlack.sortByDescending { it } - return white to black + return CapturedPieces( + whiteScore = whiteScore, + blackScore = blackScore, + capturedByWhite = remainingBlack, + capturedByBlack = remainingWhite, + ) + } } } diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/Extensions.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/Extensions.kt index 84f304e..96e541b 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/Extensions.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/Extensions.kt @@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri +import androidx.compose.foundation.lazy.LazyListState fun Byte.toBoolean() = this != 0.toByte() @@ -21,3 +22,8 @@ fun Context.browseUrl(url: String): Boolean { false } } + +fun LazyListState.isIndexVisible(index: Int): Boolean { + val end = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: layoutInfo.totalItemsCount) - 1 + return index in firstVisibleItemIndex..end +} diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/Pgn.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/Pgn.kt new file mode 100644 index 0000000..abc0c63 --- /dev/null +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/Pgn.kt @@ -0,0 +1,68 @@ +package net.theluckycoder.chess.utils + +import android.annotation.SuppressLint +import net.theluckycoder.chess.model.GameState +import net.theluckycoder.chess.model.Move +import java.text.SimpleDateFormat +import java.util.* + +object Pgn { + + private fun StringBuilder.tag(name: String, value: String) { + append('[').append(name).append(" \"").append(value).append("\"]\n") + } + + private fun StringBuilder.resultTag(gameState: GameState): String { + val value = when (gameState) { + GameState.WINNER_WHITE -> "1-0" + GameState.WINNER_BLACK -> "0-1" + GameState.DRAW -> "1/2-1/2" + else -> "*" + } + + tag("Result", value) + return value + } + + private fun StringBuilder.exportMoves(moves: List) { + append('\n') + moves.forEachIndexed { index, move -> + if (index % 2 == 0) { + append(index / 2 + 1) + append('.') + append(' ') + } + + append(move.toString()) + append(' ') + } + } + + @SuppressLint("SimpleDateFormat") + fun export( + isPlayerWhite: Boolean, + startFen: String, + moves: List, + gameState: GameState + ): String { + return buildString { + tag("Event", "Casual Game") + tag("Site", "http://theluckycoder.net/apps/chess") + tag("Date", SimpleDateFormat("yyyy.MM.dd").format(Date())) + + val (white, black) = if (isPlayerWhite) PLAYER to ENGINE else ENGINE to PLAYER + tag("White", white) + tag("Black", black) + + tag("FEN", startFen) + tag("PlyCount", moves.size.toString()) + val result = resultTag(gameState) + + exportMoves(moves) + append(result) + } + } + + private const val PLAYER = "Player" + private const val ENGINE = "Lucky Chess" +} diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/SaveManager.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/SaveManager.kt index 437193c..2f8f707 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/SaveManager.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/SaveManager.kt @@ -4,7 +4,7 @@ import android.app.Application import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import net.theluckycoder.chess.Native +import net.theluckycoder.chess.cpp.Native import net.theluckycoder.chess.model.Move import java.io.File import java.io.FileNotFoundException @@ -44,23 +44,21 @@ object SaveManager { } } - fun loadFromFile(context: Context): Boolean { - try { - context.openFileInput(SAVE_FILE_NAME).bufferedReader().use { reader -> - val lines = reader.readLines().toMutableList() + fun loadFromFile(context: Context): Boolean = try { + context.openFileInput(SAVE_FILE_NAME).bufferedReader().use { reader -> + val lines = reader.readLines().toMutableList() - val fen = lines.removeFirst() - val playerWhite = lines.removeFirst().toInt() == 1 + val fen = lines.removeFirst() + val playerWhite = lines.removeFirst().toInt() == 1 - val moves = lines.map { it.toInt() } + val moves = lines.map { it.toInt() } - return if (fen.isNotBlank() && moves.isNotEmpty()) { - Native.loadFenMoves(playerWhite, fen, moves.toIntArray()) - true - } else false - } - } catch (e: FileNotFoundException) { - return false + return if (fen.isNotBlank() && moves.isNotEmpty()) { + Native.loadFenMoves(playerWhite, fen, moves.toIntArray()) + true + } else false } + } catch (e: FileNotFoundException) { + false } } diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/SettingsDataStore.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/SettingsDataStore.kt index 85c4b39..b3970e6 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/SettingsDataStore.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/utils/SettingsDataStore.kt @@ -37,6 +37,9 @@ class SettingsDataStore private constructor(private val application: Application fun showMoveHistory(): Flow = dataStore().data.map { it[SHOW_MOVES_HISTORY] ?: DEFAULT_MOVES_HISTORY } + fun showCapturedPieces(): Flow = + dataStore().data.map { it[SHOW_CAPTURED_PIECES] ?: DEFAULT_SHOW_CAPTURED_PIECES } + fun showPieceDestination(): Flow = dataStore().data.map { it[PIECE_DESTINATIONS] ?: DEFAULT_PIECE_DESTINATIONS } @@ -65,6 +68,9 @@ class SettingsDataStore private constructor(private val application: Application preferences[HASH_SIZE] = searchOptions.hashSize } + fun allowBook(): Flow = + dataStore().data.map { it[ALLOW_BOOK] ?: true } + fun showBasicDebug(): Flow = dataStore().data.map { it[SHOW_DEBUG_BASIC] ?: false } @@ -87,6 +93,7 @@ class SettingsDataStore private constructor(private val application: Application val SHOW_COORDINATES = booleanPreferencesKey("show_coordinates") val SHOW_MOVES_HISTORY = booleanPreferencesKey("show_moves_history") + val SHOW_CAPTURED_PIECES = booleanPreferencesKey("show_captured_pieces") val PIECE_DESTINATIONS = booleanPreferencesKey("piece_destinations") val SEARCH_DEPTH = intPreferencesKey("search_depth") @@ -94,18 +101,20 @@ class SettingsDataStore private constructor(private val application: Application val SEARCH_TIME = intPreferencesKey("search_time") val THREADS = intPreferencesKey("threads") val HASH_SIZE = intPreferencesKey("hash_size") + val ALLOW_BOOK = booleanPreferencesKey("allow_book") val SHOW_DEBUG_BASIC = booleanPreferencesKey("show_debug_basic") val SHOW_DEBUG_ADVANCED = booleanPreferencesKey("show_debug_advanced") const val DEFAULT_SHOW_COORDINATES = true const val DEFAULT_MOVES_HISTORY = true + const val DEFAULT_SHOW_CAPTURED_PIECES = true const val DEFAULT_PIECE_DESTINATIONS = true // These will be overridden by the default [SearchOptions] in the Native Code const val DEFAULT_SEARCH_DEPTH = 1 const val DEFAULT_QUIET_SEARCH = true - const val DEFAULT_SEARCH_TIME = 10 + const val DEFAULT_SEARCH_TIME = 30 const val DEFAULT_THREADS = 1 const val DEFAULT_HASH_SIZE = 64 } diff --git a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/viewmodel/HomeViewModel.kt b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/viewmodel/HomeViewModel.kt index 3c1d684..a9e5d98 100644 --- a/ChessAndroid/app/src/main/java/net/theluckycoder/chess/viewmodel/HomeViewModel.kt +++ b/ChessAndroid/app/src/main/java/net/theluckycoder/chess/viewmodel/HomeViewModel.kt @@ -11,23 +11,23 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import net.theluckycoder.chess.BoardChangeListener -import net.theluckycoder.chess.Native +import net.theluckycoder.chess.cpp.BoardChangeListener +import net.theluckycoder.chess.cpp.Native +import net.theluckycoder.chess.cpp.SearchListener import net.theluckycoder.chess.model.* import net.theluckycoder.chess.utils.SaveManager import net.theluckycoder.chess.utils.SettingsDataStore -import java.util.concurrent.atomic.AtomicBoolean -class HomeViewModel(application: Application) : AndroidViewModel(application), BoardChangeListener { +class HomeViewModel(application: Application) : AndroidViewModel(application), + BoardChangeListener, SearchListener { - private val initialized = AtomicBoolean(false) + private var initialized = false val dataStore = SettingsDataStore.get(application) /* * Chess Game Data */ - private val isEngineThinkingFlow = MutableStateFlow(false) + private val isEngineBusyFlow = MutableStateFlow(false) private val playerPlayingWhiteFlow = MutableStateFlow(true) private val tilesFlow = MutableStateFlow(getEmptyTiles()) private val piecesFlow = MutableStateFlow(emptyList()) @@ -37,7 +37,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application), B private val debugStatsFlow = MutableStateFlow(DebugStats()) val playerPlayingWhite: StateFlow = playerPlayingWhiteFlow - val isEngineThinking: StateFlow = isEngineThinkingFlow + val isEngineBusy: StateFlow = isEngineBusyFlow val tiles: StateFlow> = tilesFlow val pieces: StateFlow> = piecesFlow val gameState: StateFlow = gameStateFlow @@ -54,6 +54,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application), B init { resetBoard() + Native.setSearchListener(this) viewModelScope.launch(Dispatchers.IO) { launch { @@ -66,7 +67,14 @@ class HomeViewModel(application: Application) : AndroidViewModel(application), B launch { dataStore.getEngineSettings().distinctUntilChanged().collectLatest { ensureActive() - withContext(Dispatchers.Main) { Native.setSearchOptions(it) } + SearchOptions.setNativeSearchOptions(it) + } + } + + launch { + dataStore.allowBook().distinctUntilChanged().collectLatest { + ensureActive() + Native.enableBook(it) } } @@ -88,16 +96,19 @@ class HomeViewModel(application: Application) : AndroidViewModel(application), B } fun resetBoard(playerWhite: Boolean = true) { - if (initialized.get()) { - if (isEngineThinking.value) + if (initialized) { + if (isEngineBusy.value) Native.stopSearch() + Native.initBoard(this, playerWhite) } else { // First time it is called, load the last game - initialized.set(true) - Native.initBoard(this, true) SaveManager.loadFromFile(getApplication()) + + initialized = true + + makeEngineMove() } } @@ -109,7 +120,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application), B dataStore.setDifficultyLevel(level) } - fun getPossibleMoves(square: Int) { + fun showPossibleMoves(square: Int) { val moves = Native.getPossibleMoves(square.toByte()).toList() tilesFlow.value = tilesFlow.value @@ -154,16 +165,23 @@ class HomeViewModel(application: Application) : AndroidViewModel(application), B getEmptyTiles() updatePiecesList() + makeEngineMove() + } - if (!Native.isPlayersTurn()) + private fun makeEngineMove() { + if (initialized && !Native.isPlayersTurn() && currentMoveIndex.value == movesHistory.value.lastIndex) Native.makeEngineMove() - isEngineThinkingFlow.value = Native.isEngineWorking() + isEngineBusyFlow.value = Native.isEngineBusy() + } + + override fun onFinish(success: Boolean) { + if (!success) + makeEngineMove() } private companion object { private val emptyTiles = Array(64) { Tile(it, Tile.State.None) } - fun getEmptyTiles() = emptyTiles.toList() } } diff --git a/ChessAndroid/app/src/main/res/drawable/ic_pref_book.xml b/ChessAndroid/app/src/main/res/drawable/ic_pref_book.xml new file mode 100644 index 0000000..1017f59 --- /dev/null +++ b/ChessAndroid/app/src/main/res/drawable/ic_pref_book.xml @@ -0,0 +1,10 @@ + + + diff --git a/ChessAndroid/app/src/main/res/drawable/ic_rook.xml b/ChessAndroid/app/src/main/res/drawable/ic_rook.xml new file mode 100644 index 0000000..468d26f --- /dev/null +++ b/ChessAndroid/app/src/main/res/drawable/ic_rook.xml @@ -0,0 +1,4 @@ + + + diff --git a/ChessAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ChessAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..4ae7d12 --- /dev/null +++ b/ChessAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png index 8016842..0677b39 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_background.png index 37e50d1..ed87bcc 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and b/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index a44962a..0254832 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/ChessAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png b/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png index 6aa0a85..b2de7c9 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_background.png index 5cd246a..9a08cb2 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and b/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index 39f86e5..d40424b 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/ChessAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 5effcda..53230ef 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png index 910f909..32d8dd1 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and b/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 68fa337..ffe8684 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/ChessAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 690934e..cd4620c 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png index 5e0a79f..85b5cb5 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and b/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index 4ab6b81..147c014 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/ChessAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index e9158ba..d060500 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png index 5e97f4b..93d0364 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and b/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index cb180b5..bb0937a 100644 Binary files a/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/ChessAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/ChessAndroid/app/src/main/res/values/strings.xml b/ChessAndroid/app/src/main/res/values/strings.xml index ab4bdc4..f673ec0 100644 --- a/ChessAndroid/app/src/main/res/values/strings.xml +++ b/ChessAndroid/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - Chess + Lucky Chess Time Needed: %s\nCurrent Board Evaluation: %d\n%s New Game Side: @@ -22,19 +22,20 @@ Undo Move Redo Move Close - Copy Make Engine Move Force Stop Search - - Share FEN Position + + Share Game Position Current\'s game FEN Position: Load FEN Position FEN Position Loaded FEN Position could not be loaded FEN Position can not be empty - FEN Position copied to Clipboard - FEN Position might be invalid + FEN copied to Clipboard + PGN copied to Clipboard + Copy FEN + Copy PGN Appearance @@ -42,6 +43,8 @@ Display Coordinates alongside the board Moves History Display all previously made moves + Captured Pieces + Display all captured pieces by either side Piece Destinations Display legal moves for selected pieces @@ -54,6 +57,8 @@ The maximum time the engine is allowed to think in seconds Other Algorithm Settings + Allow Opening Book + Allow the engine to use an Opening Book in the beginning of the game in order to make random moves Thread Count Number of Search Threads to be used Cache Size diff --git a/ChessAndroid/build.gradle b/ChessAndroid/build.gradle index b779d68..2a68deb 100644 --- a/ChessAndroid/build.gradle +++ b/ChessAndroid/build.gradle @@ -1,22 +1,25 @@ buildscript { ext.kotlin_version = "1.4.32" - ext.compose_version = "1.0.0-beta04" + ext.compose_version = "1.0.0-beta06" repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0-alpha14' + classpath 'com.android.tools.build:gradle:7.0.0-alpha15' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } +plugins { + id "com.github.ben-manes.versions" version "0.38.0" +} + allprojects { repositories { google() mavenCentral() - jcenter() } } diff --git a/ChessAndroid/gradle/wrapper/gradle-wrapper.properties b/ChessAndroid/gradle/wrapper/gradle-wrapper.properties index db7e6f2..2680cfe 100644 --- a/ChessAndroid/gradle/wrapper/gradle-wrapper.properties +++ b/ChessAndroid/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip -distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.1-all.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME