add custom chess engine

This commit is contained in:
Josh-Heaps
2026-06-08 13:58:22 -06:00
parent d2ae34f530
commit c3280aa6d3
42 changed files with 3033 additions and 168 deletions
+42
View File
@@ -0,0 +1,42 @@
cmake_minimum_required(VERSION 3.20)
project(chess_engine LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Shared library: chess_engine.dll (Windows) / libchess_engine.so (Linux).
add_library(chess_engine SHARED
src/chess_engine.cpp
src/bitboard.cpp
src/zobrist.cpp
src/position.cpp
src/movegen.cpp
src/uci.cpp
src/perft.cpp)
target_include_directories(chess_engine PUBLIC include)
target_compile_definitions(chess_engine PRIVATE CHESS_ENGINE_BUILD)
set_target_properties(chess_engine PROPERTIES
OUTPUT_NAME chess_engine
POSITION_INDEPENDENT_CODE ON) # -fPIC on Linux (required for .so)
# Export only the symbols marked with the CHESS_API macro.
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
if (MSVC)
target_compile_options(chess_engine PRIVATE
$<$<CONFIG:Release>:/O2 /GL /DNDEBUG /arch:AVX2>
$<$<CONFIG:Debug>:/Od /Zi>) # /Zi => .pdb for mixed-mode debugging
target_link_options(chess_engine PRIVATE
$<$<CONFIG:Release>:/LTCG>
$<$<CONFIG:Debug>:/DEBUG>)
else()
# Portable server baseline. Do NOT use -march=native: the build host may have
# instructions the server lacks (SIGILL at runtime). Bump only once the server
# CPU floor is confirmed.
target_compile_options(chess_engine PRIVATE
$<$<CONFIG:Release>:-O3 -flto -DNDEBUG -march=x86-64-v2>
$<$<CONFIG:Debug>:-O0 -g>)
endif()
@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>18.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{2579bbbc-1830-4342-bc10-0a4182dc84c7}</ProjectGuid>
<RootNamespace>chessengine</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;CHESS_ENGINE_BUILD;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>$(ProjectDir)..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;CHESS_ENGINE_BUILD;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>$(ProjectDir)..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;CHESS_ENGINE_BUILD;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>$(ProjectDir)..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;CHESS_ENGINE_BUILD;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>$(ProjectDir)..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\src\chess_engine.cpp" />
<ClCompile Include="..\src\bitboard.cpp" />
<ClCompile Include="..\src\zobrist.cpp" />
<ClCompile Include="..\src\position.cpp" />
<ClCompile Include="..\src\movegen.cpp" />
<ClCompile Include="..\src\uci.cpp" />
<ClCompile Include="..\src\perft.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\include\chess_engine.h" />
<ClInclude Include="..\src\types.h" />
<ClInclude Include="..\src\bitboard.h" />
<ClInclude Include="..\src\zobrist.h" />
<ClInclude Include="..\src\position.h" />
<ClInclude Include="..\src\movegen.h" />
<ClInclude Include="..\src\uci.h" />
<ClInclude Include="..\src\perft.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
<Target Name="CopyEngineToResources" AfterTargets="Build">
<!-- Copy the DLL plus its .pdb so native breakpoints bind when the .NET host loads the engine. -->
<Copy SourceFiles="$(TargetPath);$(TargetDir)$(TargetName).pdb" DestinationFolder="$(ProjectDir)..\..\..\JoshHeaps.Net\Resources\" SkipUnchangedFiles="false" />
</Target>
</Project>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\chess_engine.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\bitboard.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\zobrist.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\position.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\movegen.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\uci.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\perft.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\include\chess_engine.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\types.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\bitboard.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\zobrist.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\position.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\movegen.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\uci.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\perft.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>
@@ -0,0 +1,80 @@
/* chess_engine.h - C ABI for a swappable chess engine.
*
* Contract: FEN string in, UCI move string out (e.g. "e2e4", "e7e8q").
* The C# host (CustomChessEngine) owns all buffers. The engine NEVER allocates
* memory that the host must free. Functions are thread-compatible per-handle only:
* do NOT call two functions on the SAME handle concurrently. Different handles
* are independent.
*/
#ifndef CHESS_ENGINE_H
#define CHESS_ENGINE_H
#include <stddef.h>
/* ---- Export / calling-convention macro (MSVC + GCC/Clang) ---- */
#if defined(_WIN32)
#ifdef CHESS_ENGINE_BUILD
#define CHESS_API __declspec(dllexport)
#else
#define CHESS_API __declspec(dllimport)
#endif
#define CHESS_CALL __cdecl /* explicit; matches C# CallingConvention.Cdecl */
#else
#define CHESS_API __attribute__((visibility("default")))
#define CHESS_CALL /* SysV default; no decoration needed */
#endif
#ifdef __cplusplus
extern "C" { /* prevent C++ name mangling */
#endif
/* Opaque handle. The host treats this as a token and never dereferences it.
* Internally it points to your engine state object. */
typedef struct ChessEngine* EngineHandle;
/* Return codes. 0 == success; negative == error. Keep these values stable. */
enum {
CHESS_OK = 0,
CHESS_ERR_NULL_HANDLE = -1, /* handle was null/invalid */
CHESS_ERR_BAD_FEN = -2, /* fen failed to parse */
CHESS_ERR_NO_MOVE = -3, /* no legal move (mate/stalemate) */
CHESS_ERR_BUFFER = -4, /* out_buf too small for the move + NUL */
CHESS_ERR_INTERNAL = -5 /* unexpected engine failure */
};
/* Create an engine instance.
* options: optional null-terminated UTF-8 config string (may be NULL),
* e.g. "skill=20;hash=256". Parse however you like; ignore for now.
* returns: a valid EngineHandle, or NULL on allocation failure. */
CHESS_API EngineHandle CHESS_CALL engine_create(const char* options);
/* Set a single option by name (optional; may no-op for now).
* returns CHESS_OK or a negative code. */
CHESS_API int CHESS_CALL engine_set_option(EngineHandle engine,
const char* name,
const char* value);
/* Compute the best move for the given position.
* engine : handle from engine_create.
* fen : null-terminated UTF-8 FEN of the position to move from.
* out_buf : host-owned buffer the engine writes the UCI move into,
* as a null-terminated ASCII string (e.g. "e2e4\0").
* out_len : capacity of out_buf in bytes (host passes >= 8).
* returns CHESS_OK on success (out_buf now holds the move), else negative.
* MUST NOT write more than out_len bytes including the NUL terminator. */
CHESS_API int CHESS_CALL engine_best_move(EngineHandle engine,
const char* fen,
char* out_buf,
int out_len);
/* Write the engine version string into out_buf (null-terminated).
* returns CHESS_OK or CHESS_ERR_BUFFER. */
CHESS_API int CHESS_CALL engine_version(char* out_buf, int out_len);
/* Destroy an instance created by engine_create. Safe to call with NULL. */
CHESS_API void CHESS_CALL engine_destroy(EngineHandle engine);
#ifdef __cplusplus
}
#endif
#endif /* CHESS_ENGINE_H */
+146
View File
@@ -0,0 +1,146 @@
#include "bitboard.h"
#include <cstdlib>
namespace chess {
Bitboard PawnAttacks[COLOR_NB][SQUARE_NB];
Bitboard KnightAttacks[SQUARE_NB];
Bitboard KingAttacks[SQUARE_NB];
Magic BishopMagics[SQUARE_NB];
Magic RookMagics[SQUARE_NB];
// Backing storage the magics index into (fancy-magic sizes).
static Bitboard RookTable[102400];
static Bitboard BishopTable[5248];
namespace {
int file_distance(Square a, Square b) {
return std::abs(int(file_of(a)) - int(file_of(b)));
}
// Slow, edge-aware ray attack used only to build the tables.
Bitboard sliding_attack(const int* dirs, Square sq, Bitboard occ) {
Bitboard attacks = 0;
for (int i = 0; i < 4; ++i) {
Square prev = sq;
int t = int(sq) + dirs[i];
while (t >= 0 && t < 64 && file_distance(Square(t), prev) <= 1) {
attacks |= square_bb(Square(t));
if (occ & square_bb(Square(t))) break;
prev = Square(t);
t += dirs[i];
}
}
return attacks;
}
// Relevant-occupancy mask: the ray squares excluding board edges.
Bitboard slider_mask(const int* dirs, Square sq) {
Bitboard edges = ((RANK_1_BB | RANK_8_BB) & ~rank_bb(rank_of(sq)))
| ((FILE_A_BB | FILE_H_BB) & ~file_bb(file_of(sq)));
return sliding_attack(dirs, sq, 0) & ~edges;
}
// Deterministic xorshift PRNG (fixed seed -> reproducible magics).
struct PRNG {
uint64_t s;
explicit PRNG(uint64_t seed) : s(seed) {}
uint64_t next() {
s ^= s >> 12; s ^= s << 25; s ^= s >> 27;
return s * 2685821657736338717ULL;
}
// Few set bits -> better magic candidates.
uint64_t sparse() { return next() & next() & next(); }
};
void init_magics(const int* dirs, Magic magics[], Bitboard table[]) {
PRNG rng(0x9E3779B97F4A7C15ull); // fixed seed -> reproducible magics
Bitboard occupancy[4096];
Bitboard reference[4096];
int epoch[4096] = {};
int currentEpoch = 0;
size_t offset = 0;
for (int sq = 0; sq < 64; ++sq) {
Magic& m = magics[sq];
m.mask = slider_mask(dirs, Square(sq));
m.shift = 64 - popcount(m.mask);
m.attacks = table + offset;
// Enumerate every subset of the mask (Carry-Rippler).
Bitboard b = 0;
int size = 0;
do {
occupancy[size] = b;
reference[size] = sliding_attack(dirs, Square(sq), b);
++size;
b = (b - m.mask) & m.mask;
} while (b);
// Search for a magic that maps subsets to indices collision-free
// (collisions are fine only when the attack set is identical).
for (;;) {
Bitboard magic;
do {
magic = rng.sparse();
} while (popcount((m.mask * magic) >> 56) < 6);
m.magic = magic;
++currentEpoch;
bool ok = true;
for (int i = 0; i < size; ++i) {
unsigned idx = m.index(occupancy[i]);
if (epoch[idx] < currentEpoch) {
epoch[idx] = currentEpoch;
m.attacks[idx] = reference[i];
} else if (m.attacks[idx] != reference[i]) {
ok = false;
break;
}
}
if (ok) break;
}
offset += size;
}
}
} // namespace
void init_bitboards() {
for (int s = 0; s < 64; ++s) {
Bitboard b = square_bb(Square(s));
PawnAttacks[WHITE][s] = ((b & ~FILE_H_BB) << 9) | ((b & ~FILE_A_BB) << 7);
PawnAttacks[BLACK][s] = ((b & ~FILE_A_BB) >> 9) | ((b & ~FILE_H_BB) >> 7);
const int knightDirs[8] = { 17, 15, 10, 6, -6, -10, -15, -17 };
Bitboard kn = 0;
for (int d : knightDirs) {
int t = s + d;
if (t >= 0 && t < 64 && file_distance(Square(t), Square(s)) <= 2)
kn |= square_bb(Square(t));
}
KnightAttacks[s] = kn;
const int kingDirs[8] = { 8, -8, 1, -1, 9, 7, -7, -9 };
Bitboard kg = 0;
for (int d : kingDirs) {
int t = s + d;
if (t >= 0 && t < 64 && file_distance(Square(t), Square(s)) <= 1)
kg |= square_bb(Square(t));
}
KingAttacks[s] = kg;
}
const int rookDirs[4] = { 8, -8, 1, -1 };
const int bishopDirs[4] = { 9, 7, -7, -9 };
init_magics(rookDirs, RookMagics, RookTable);
init_magics(bishopDirs, BishopMagics, BishopTable);
}
} // namespace chess
+87
View File
@@ -0,0 +1,87 @@
// Bitboard utilities and precomputed attack tables. Sliders use magic
// bitboards; the tables are built once by init_bitboards() (called from
// engine_create) and are read-only afterwards.
#ifndef CHESS_BITBOARD_H
#define CHESS_BITBOARD_H
#include "types.h"
#if defined(_MSC_VER)
#include <intrin.h>
#endif
namespace chess {
constexpr Bitboard FILE_A_BB = 0x0101010101010101ULL;
constexpr Bitboard FILE_H_BB = 0x8080808080808080ULL;
constexpr Bitboard RANK_1_BB = 0x00000000000000FFULL;
constexpr Bitboard RANK_8_BB = 0xFF00000000000000ULL;
inline Bitboard square_bb(Square s) { return 1ULL << s; }
inline Bitboard file_bb(File f) { return FILE_A_BB << f; }
inline Bitboard rank_bb(Rank r) { return RANK_1_BB << (8 * int(r)); }
inline int popcount(Bitboard b) {
#if defined(_MSC_VER)
return int(__popcnt64(b));
#else
return __builtin_popcountll(b);
#endif
}
inline Square lsb(Bitboard b) {
#if defined(_MSC_VER)
unsigned long i;
_BitScanForward64(&i, b);
return Square(i);
#else
return Square(__builtin_ctzll(b));
#endif
}
// Returns the least-significant square and clears it from b.
inline Square pop_lsb(Bitboard& b) {
Square s = lsb(b);
b &= b - 1;
return s;
}
inline bool more_than_one(Bitboard b) { return b & (b - 1); }
// Precomputed leaper attacks (filled by init_bitboards).
extern Bitboard PawnAttacks[COLOR_NB][SQUARE_NB];
extern Bitboard KnightAttacks[SQUARE_NB];
extern Bitboard KingAttacks[SQUARE_NB];
struct Magic {
Bitboard mask;
Bitboard magic;
Bitboard* attacks;
unsigned shift;
unsigned index(Bitboard occ) const {
return unsigned(((occ & mask) * magic) >> shift);
}
};
extern Magic BishopMagics[SQUARE_NB];
extern Magic RookMagics[SQUARE_NB];
inline Bitboard bishop_attacks(Square s, Bitboard occ) {
const Magic& m = BishopMagics[s];
return m.attacks[m.index(occ)];
}
inline Bitboard rook_attacks(Square s, Bitboard occ) {
const Magic& m = RookMagics[s];
return m.attacks[m.index(occ)];
}
inline Bitboard queen_attacks(Square s, Bitboard occ) {
return bishop_attacks(s, occ) | rook_attacks(s, occ);
}
// Must be called once before any attack query (engine_create does this).
void init_bitboards();
} // namespace chess
#endif // CHESS_BITBOARD_H
+283
View File
@@ -0,0 +1,283 @@
/* chess_engine.cpp - the DLL boundary (extern "C" ABI).
*
* The rules layer (board, move generation, make/unmake, hashing, perft) lives in
* the other src/*.cpp files and is ready to use. engine_best_move is intentionally
* left for YOU: that is where your search/evaluation goes. Everything below the
* FEN-in / UCI-out boundary should stay native — the managed side crosses it once
* per move.
*/
#ifndef CHESS_ENGINE_BUILD
#define CHESS_ENGINE_BUILD /* fallback when not building via CMake (which defines it) */
#endif
#pragma once
#include "chess_engine.h"
#include "bitboard.h"
#include "zobrist.h"
#include "position.h"
#include "movegen.h"
#include "uci.h"
#include <cstdlib>
#include <cstring>
#include <limits>
#include <new>
#include <string>
#include <memory>
/* Internal engine state. Put your search tables, transposition table, etc. here. */
struct ChessEngine {
int skill = 20; /* 1..20 from the UI; controls search depth */
};
static int copy_out(const char* src, char* out_buf, int out_len) {
if (!out_buf || out_len <= 0) return CHESS_ERR_BUFFER;
const size_t need = std::strlen(src) + 1; /* + NUL */
if (need > static_cast<size_t>(out_len)) return CHESS_ERR_BUFFER;
std::memcpy(out_buf, src, need);
return CHESS_OK;
}
/* Attack tables and Zobrist keys are global and read-only after this runs. */
static void ensure_initialized() {
static bool done = false;
if (done) return;
chess::init_bitboards();
chess::Zobrist::init();
done = true;
}
/* Pulls "skill=N" out of the engine_create options string; clamps to the UI's 1..20. */
static int parse_skill(const char* options, int fallback) {
if (!options) return fallback;
const char* p = std::strstr(options, "skill=");
if (!p) return fallback;
int v = std::atoi(p + 6);
return v < 1 ? 1 : v > 20 ? 20 : v;
}
/* Maps the 1..20 difficulty to a search depth. Kept modest: the search has no move
* ordering or quiescence yet, so deep fixed-depth runs get expensive quickly. */
static int depth_for_skill(int skill) {
return skill; /* skill 1 -> 2 plies ... skill 20 -> 7 plies */
}
/* Positional multiplier in [0.5, 2.0] based on a square's distance from the four
* center squares (d4/e4/d5/e5): 2.0 dead center, 0.5 in a corner, scaling linearly.
* Multiply a piece's base value by this to reward central placement. */
static double center_multiplier(chess::Square s) {
/* |2*coord - 7| is the distance from center in half-squares: 1 (center) .. 7 (edge). */
int fileDist = std::abs(2 * int(chess::file_of(s)) - 7);
int rankDist = std::abs(2 * int(chess::rank_of(s)) - 7);
int dist = fileDist > rankDist ? fileDist : rankDist; /* Chebyshev distance, 1 .. 7 */
return dist * 20; /* 1 -> 2.0, 7 -> 0.5 */
}
static int piece_mobility(const chess::Position& pos, chess::Square s, chess::Piece pc, chess::Color c) {
chess::Bitboard occ = pos.pieces();
chess::Bitboard targets;
switch (chess::type_of(pc)) {
case chess::KNIGHT: targets = chess::KnightAttacks[s]; break;
case chess::BISHOP: targets = chess::bishop_attacks(s, occ); break;
case chess::ROOK: targets = chess::rook_attacks(s, occ); break;
case chess::QUEEN: targets = chess::queen_attacks(s, occ); break;
case chess::KING: targets = chess::KingAttacks[s]; break;
default: return 0; // pawns: mobility usually handled via push/attack separately
}
return chess::popcount(targets & ~pos.pieces(c)); // exclude squares blocked by own pieces
}
static chess::Bitboard front_span(chess::Color c, chess::Square s) {
chess::File f = file_of(s);
chess::Bitboard files = file_bb(f);
if (f > chess::FILE_A) files |= chess::file_bb(chess::File(f - 1));
if (f < chess::FILE_H) files |= chess::file_bb(chess::File(f + 1));
// Pawns never sit on rank 1 or 8, so rank is 1..6 and these shifts
// are always in [8,56] — no shift-by-64 UB to guard against.
chess::Rank r = rank_of(s);
chess::Bitboard ahead = (c == chess::WHITE) ? (~0ULL << (8 * (r + 1))) // ranks > r
: ((1ULL << (8 * r)) - 1); // ranks < r
return files & ahead;
}
static chess::Bitboard front_span_file_only(chess::Color c, chess::Square s) {
chess::File f = file_of(s);
chess::Bitboard files = file_bb(f);
// Pawns never sit on rank 1 or 8, so rank is 1..6 and these shifts
// are always in [8,56] — no shift-by-64 UB to guard against.
chess::Rank r = rank_of(s);
chess::Bitboard ahead = (c == chess::WHITE) ? (~0ULL << (8 * (r + 1))) // ranks > r
: ((1ULL << (8 * r)) - 1); // ranks < r
return files & ahead;
}
static int evaluatePawn(const chess::Position& pos, const chess::Color c, const chess::Square s) {
chess::Bitboard span = front_span(c, s);
chess::Bitboard file_span = front_span_file_only(c, s);
chess::Rank r = rank_of(s);
int squaresToPromotion = (c == chess::WHITE) ? (chess::RANK_8 - r) : (r - chess::RANK_1);;
bool isPassed = !(span & pos.pieces(~c, chess::PAWN));
bool isBlocked = (file_span & pos.pieces(c, chess::PAWN)) | (file_span & pos.pieces(~c, chess::PAWN));
bool isDoubled = (file_span & pos.pieces(c, chess::PAWN));
int score = 100;
if (isPassed && !isBlocked)
score += squaresToPromotion * 10; // Bonus for passed pawns, more as they get closer to promotion
if (isDoubled)
score -= 20; // Penalty for doubled pawns
if (isBlocked)
score -= 20; // Penalty for blocked pawns
return score;
}
static int evaluatePiece(const chess::Position& pos, const chess::Square& s, const chess::Piece& pc, const chess::Color& c) {
int score = 0;
switch (chess::type_of(pc)) {
case chess::PAWN: score = evaluatePawn(pos, c, s); break;
case chess::KNIGHT: score = 320; break;
case chess::BISHOP: score = 330; break;
case chess::ROOK: score = 500; break;
case chess::QUEEN: score = 900; break;
default: return 0;
}
score += center_multiplier(s);
score += piece_mobility(pos, s, pc, c) * 10;
return score;
}
static int evaluate(const chess::Position& pos) {
int score = 0;
chess::Bitboard white = pos.pieces(chess::WHITE);
while (white) {
chess::Square s = chess::pop_lsb(white);
chess::Piece pc = pos.piece_on(s);
chess::Color c = chess::color_of(pc);
score += evaluatePiece(pos, s, pc, c);
}
chess::Bitboard black = pos.pieces(chess::BLACK);
while (black) {
chess::Square s = chess::pop_lsb(black);
chess::Piece pc = pos.piece_on(s);
chess::Color c = chess::color_of(pc);
score -= evaluatePiece(pos, s, pc, c);
}
return score;
}
extern "C" {
CHESS_API EngineHandle CHESS_CALL engine_create(const char* options) {
ensure_initialized();
auto* e = new (std::nothrow) ChessEngine();
if (!e) return nullptr;
e->skill = parse_skill(options, e->skill);
return e;
}
CHESS_API int CHESS_CALL engine_set_option(EngineHandle engine,
const char* /*name*/,
const char* /*value*/) {
if (!engine) return CHESS_ERR_NULL_HANDLE;
return CHESS_OK; /* TODO: store options */
}
static int alpha_beta(chess::Position& pos, int depth, int maxDepth, int bestForWhite, int bestForBlack, bool whiteToMove) {
if (depth == maxDepth)
return evaluate(pos);
chess::MoveList moves;
pos.generate_legal(moves);
if (moves.size() == 0)
return pos.is_draw() ? 0 : whiteToMove ? -200000 + depth : 200000 - depth;
for (int i = 0; i < moves.size(); i++) {
chess::Move move = moves.moves[i];
pos.do_move(move);
int moveScore = alpha_beta(pos, depth + 1, maxDepth, bestForWhite, bestForBlack, !whiteToMove);
if (whiteToMove) {
if (moveScore >= bestForBlack) {
pos.undo_move(move);
return bestForBlack;
}
if (moveScore > bestForWhite)
bestForWhite = moveScore;
}
else {
if (moveScore <= bestForWhite) {
pos.undo_move(move);
return bestForWhite;
}
if (moveScore < bestForBlack)
bestForBlack = moveScore;
}
pos.undo_move(move);
}
return whiteToMove ? bestForWhite : bestForBlack;
}
CHESS_API int CHESS_CALL engine_best_move(EngineHandle engine,
const char* fen,
char* out_buf,
int out_len) {
if (!engine) return CHESS_ERR_NULL_HANDLE;
if (!fen || !*fen) return CHESS_ERR_BAD_FEN;
auto held = std::make_unique<chess::Position>(chess::Position::from_fen(fen));
chess::Position& pos = *held;
bool whiteToMove = pos.side_to_move() == chess::WHITE;
chess::MoveList moves;
pos.generate_legal(moves);
if (moves.size() == 0)
return CHESS_ERR_NO_MOVE;
int maxDepth = depth_for_skill(engine->skill);
int bestForWhite = std::numeric_limits<int>::min();
int bestForBlack = std::numeric_limits<int>::max();
chess::Move bestMove = moves.moves[0];
for (int i = 0; i < moves.size(); i++) {
chess::Move move = moves.moves[i];
pos.do_move(move);
int score = alpha_beta(pos, 1, maxDepth, bestForWhite, bestForBlack, !whiteToMove);
pos.undo_move(move);
if (whiteToMove && score > bestForWhite) {
bestForWhite = score;
bestMove = move;
}
else if (!whiteToMove && score < bestForBlack) {
bestForBlack = score;
bestMove = move;
}
}
return copy_out(chess::move_to_uci(bestMove).c_str(), out_buf, out_len);
}
CHESS_API int CHESS_CALL engine_version(char* out_buf, int out_len) {
return copy_out("custom-engine 0.1.0", out_buf, out_len);
}
CHESS_API void CHESS_CALL engine_destroy(EngineHandle engine) {
delete engine; /* delete nullptr is safe */
}
} /* extern "C" */
+134
View File
@@ -0,0 +1,134 @@
#include "movegen.h"
namespace chess {
namespace {
void add_promotions(MoveList& list, Square from, Square to) {
list.add(Move::make(from, to, PROMOTION, QUEEN));
list.add(Move::make(from, to, PROMOTION, ROOK));
list.add(Move::make(from, to, PROMOTION, BISHOP));
list.add(Move::make(from, to, PROMOTION, KNIGHT));
}
void generate_castling(const Position& pos, MoveList& list) {
if (pos.in_check()) return;
Color us = pos.side_to_move(), them = ~us;
Bitboard occ = pos.pieces();
auto attacked = [&](Square sq) { return (pos.attackers_to(sq) & pos.pieces(them)) != 0; };
if (us == WHITE) {
if (pos.can_castle(WHITE, KINGSIDE) &&
!(occ & (square_bb(F1) | square_bb(G1))) && !attacked(F1) && !attacked(G1))
list.add(Move::make(E1, G1, CASTLING));
if (pos.can_castle(WHITE, QUEENSIDE) &&
!(occ & (square_bb(B1) | square_bb(C1) | square_bb(D1))) && !attacked(D1) && !attacked(C1))
list.add(Move::make(E1, C1, CASTLING));
} else {
if (pos.can_castle(BLACK, KINGSIDE) &&
!(occ & (square_bb(F8) | square_bb(G8))) && !attacked(F8) && !attacked(G8))
list.add(Move::make(E8, G8, CASTLING));
if (pos.can_castle(BLACK, QUEENSIDE) &&
!(occ & (square_bb(B8) | square_bb(C8) | square_bb(D8))) && !attacked(D8) && !attacked(C8))
list.add(Move::make(E8, C8, CASTLING));
}
}
} // namespace
void generate_pseudo(const Position& pos, MoveList& list) {
Color us = pos.side_to_move(), them = ~us;
Bitboard occ = pos.pieces();
Bitboard targets = ~pos.pieces(us); // empty squares or enemy pieces
Bitboard theirs = pos.pieces(them);
// Pawns
int push = (us == WHITE) ? 8 : -8;
Rank promoRank = (us == WHITE) ? RANK_8 : RANK_1;
Rank startRank = (us == WHITE) ? RANK_2 : RANK_7;
Bitboard b = pos.pieces(us, PAWN);
while (b) {
Square s = pop_lsb(b);
Square t = Square(int(s) + push);
if (!(occ & square_bb(t))) {
if (rank_of(t) == promoRank) {
add_promotions(list, s, t);
} else {
list.add(Move::make(s, t));
if (rank_of(s) == startRank) {
Square t2 = Square(int(t) + push);
if (!(occ & square_bb(t2))) list.add(Move::make(s, t2));
}
}
}
Bitboard caps = PawnAttacks[us][s] & theirs;
while (caps) {
Square c = pop_lsb(caps);
if (rank_of(c) == promoRank) add_promotions(list, s, c);
else list.add(Move::make(s, c));
}
if (pos.ep_square() != SQ_NONE && (PawnAttacks[us][s] & square_bb(pos.ep_square())))
list.add(Move::make(s, pos.ep_square(), EN_PASSANT));
}
// Knights
b = pos.pieces(us, KNIGHT);
while (b) {
Square s = pop_lsb(b);
Bitboard a = KnightAttacks[s] & targets;
while (a) list.add(Move::make(s, pop_lsb(a)));
}
// Bishops
b = pos.pieces(us, BISHOP);
while (b) {
Square s = pop_lsb(b);
Bitboard a = bishop_attacks(s, occ) & targets;
while (a) list.add(Move::make(s, pop_lsb(a)));
}
// Rooks
b = pos.pieces(us, ROOK);
while (b) {
Square s = pop_lsb(b);
Bitboard a = rook_attacks(s, occ) & targets;
while (a) list.add(Move::make(s, pop_lsb(a)));
}
// Queens
b = pos.pieces(us, QUEEN);
while (b) {
Square s = pop_lsb(b);
Bitboard a = queen_attacks(s, occ) & targets;
while (a) list.add(Move::make(s, pop_lsb(a)));
}
// King (non-castling)
{
Square s = pos.king_square(us);
Bitboard a = KingAttacks[s] & targets;
while (a) list.add(Move::make(s, pop_lsb(a)));
}
}
void Position::generate_legal(MoveList& list) {
list.count = 0;
MoveList pseudo;
generate_pseudo(*this, pseudo);
Color us = sideToMove;
for (Move m : pseudo) {
do_move(m);
// After do_move, sideToMove is the opponent; the move is legal iff the
// side that just moved did not leave its own king attacked.
bool legal = (attackers_to(king_square(us)) & pieces(sideToMove)) == 0;
undo_move(m);
if (legal) list.add(m);
}
generate_castling(*this, list); // already fully legal
}
} // namespace chess
+17
View File
@@ -0,0 +1,17 @@
// Legal move generation. generate_legal is a method on Position (declared
// there); this header exists so other translation units can pull in the
// pseudo-legal generator if they ever want it.
#ifndef CHESS_MOVEGEN_H
#define CHESS_MOVEGEN_H
#include "position.h"
namespace chess {
// Generates pseudo-legal moves (ignores leaving your own king in check).
// Position::generate_legal filters these. Castling is generated fully-legal.
void generate_pseudo(const Position& pos, MoveList& list);
} // namespace chess
#endif // CHESS_MOVEGEN_H
+22
View File
@@ -0,0 +1,22 @@
#include "perft.h"
namespace chess {
uint64_t perft(Position& pos, int depth) {
if (depth == 0) return 1;
MoveList list;
pos.generate_legal(list);
if (depth == 1) return uint64_t(list.size());
uint64_t nodes = 0;
for (Move m : list) {
pos.do_move(m);
nodes += perft(pos, depth - 1);
pos.undo_move(m);
}
return nodes;
}
} // namespace chess
+14
View File
@@ -0,0 +1,14 @@
// Perft: counts the leaf nodes of the legal move tree to a given depth.
// The standard correctness test for move generation + make/unmake.
#ifndef CHESS_PERFT_H
#define CHESS_PERFT_H
#include "position.h"
namespace chess {
uint64_t perft(Position& pos, int depth);
} // namespace chess
#endif // CHESS_PERFT_H
+347
View File
@@ -0,0 +1,347 @@
#include "position.h"
#include "zobrist.h"
#include <cctype>
#include <cstdio>
#include <cstring>
#include <memory>
namespace chess {
namespace {
// Bits of castling rights that are revoked when a piece leaves/arrives a square
// (covers king moves, rook moves, and rook captures uniformly).
int castling_mask(Square s) {
switch (s) {
case E1: return WHITE_OO | WHITE_OOO;
case A1: return WHITE_OOO;
case H1: return WHITE_OO;
case E8: return BLACK_OO | BLACK_OOO;
case A8: return BLACK_OOO;
case H8: return BLACK_OO;
default: return 0;
}
}
char piece_to_char(Piece p) {
const char* w = " PNBRQK";
char c = w[type_of(p)];
return color_of(p) == BLACK ? char(std::tolower(c)) : c;
}
} // namespace
void Position::put_piece(Piece pc, Square s) {
board[s] = pc;
byTypeBB[type_of(pc)] |= square_bb(s);
byColorBB[color_of(pc)] |= square_bb(s);
zkey ^= Zobrist::psq[pc][s];
}
void Position::remove_piece(Square s) {
Piece pc = board[s];
byTypeBB[type_of(pc)] ^= square_bb(s);
byColorBB[color_of(pc)] ^= square_bb(s);
board[s] = NO_PIECE;
zkey ^= Zobrist::psq[pc][s];
}
void Position::move_piece(Square from, Square to) {
Piece pc = board[from];
Bitboard fromTo = square_bb(from) | square_bb(to);
byTypeBB[type_of(pc)] ^= fromTo;
byColorBB[color_of(pc)] ^= fromTo;
board[from] = NO_PIECE;
board[to] = pc;
zkey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to];
}
bool Position::can_castle(Color c, CastlingSide side) const {
int r = (c == WHITE) ? (side == KINGSIDE ? WHITE_OO : WHITE_OOO)
: (side == KINGSIDE ? BLACK_OO : BLACK_OOO);
return (castlingRights & r) != 0;
}
Position Position::from_fen(std::string_view fen) {
auto held = std::make_unique<Position>();
Position& p = *held;
std::memset(p.byTypeBB, 0, sizeof(p.byTypeBB));
std::memset(p.byColorBB, 0, sizeof(p.byColorBB));
for (int s = 0; s < SQUARE_NB; ++s) p.board[s] = NO_PIECE;
p.sideToMove = WHITE;
p.castlingRights = NO_CASTLING;
p.epSquare = SQ_NONE;
p.rule50 = 0;
p.gamePly = 0;
p.zkey = 0;
p.undoCount = 0;
size_t i = 0;
int rank = 7, file = 0;
// 1) piece placement
for (; i < fen.size() && fen[i] != ' '; ++i) {
char c = fen[i];
if (c == '/') { --rank; file = 0; }
else if (std::isdigit((unsigned char)c)) { file += c - '0'; }
else {
Color col = std::isupper((unsigned char)c) ? WHITE : BLACK;
PieceType pt = NO_PIECE_TYPE;
switch (std::tolower((unsigned char)c)) {
case 'p': pt = PAWN; break;
case 'n': pt = KNIGHT; break;
case 'b': pt = BISHOP; break;
case 'r': pt = ROOK; break;
case 'q': pt = QUEEN; break;
case 'k': pt = KING; break;
}
if (pt != NO_PIECE_TYPE)
p.put_piece(make_piece(col, pt), make_square(File(file), Rank(rank)));
++file;
}
}
auto skip_space = [&] { while (i < fen.size() && fen[i] == ' ') ++i; };
// 2) side to move
skip_space();
if (i < fen.size()) { p.sideToMove = (fen[i] == 'b') ? BLACK : WHITE; ++i; }
// 3) castling rights
skip_space();
for (; i < fen.size() && fen[i] != ' '; ++i) {
switch (fen[i]) {
case 'K': p.castlingRights |= WHITE_OO; break;
case 'Q': p.castlingRights |= WHITE_OOO; break;
case 'k': p.castlingRights |= BLACK_OO; break;
case 'q': p.castlingRights |= BLACK_OOO; break;
default: break; // '-' or Chess960 letters
}
}
// 4) en passant
skip_space();
if (i < fen.size() && fen[i] != '-' && fen[i] != ' ') {
File f = File(fen[i] - 'a');
Rank r = Rank(fen[i + 1] - '1');
p.epSquare = make_square(f, r);
i += 2;
} else if (i < fen.size() && fen[i] == '-') {
++i;
}
// 5) halfmove clock
skip_space();
int halfmove = 0;
for (; i < fen.size() && std::isdigit((unsigned char)fen[i]); ++i)
halfmove = halfmove * 10 + (fen[i] - '0');
p.rule50 = halfmove;
// 6) fullmove number
skip_space();
int fullmove = 1;
if (i < fen.size() && std::isdigit((unsigned char)fen[i])) {
fullmove = 0;
for (; i < fen.size() && std::isdigit((unsigned char)fen[i]); ++i)
fullmove = fullmove * 10 + (fen[i] - '0');
}
p.gamePly = (fullmove - 1) * 2 + (p.sideToMove == BLACK ? 1 : 0);
// finalize the hash
if (p.sideToMove == BLACK) p.zkey ^= Zobrist::side;
p.zkey ^= Zobrist::castling[p.castlingRights];
if (p.epSquare != SQ_NONE) p.zkey ^= Zobrist::enpassant[file_of(p.epSquare)];
p.repKeys[0] = p.zkey;
p.repCount = 1;
return p;
}
std::string Position::to_fen() const {
std::string s;
for (int r = 7; r >= 0; --r) {
int empty = 0;
for (int f = 0; f < 8; ++f) {
Piece pc = board[make_square(File(f), Rank(r))];
if (pc == NO_PIECE) { ++empty; continue; }
if (empty) { s += char('0' + empty); empty = 0; }
s += piece_to_char(pc);
}
if (empty) s += char('0' + empty);
if (r) s += '/';
}
s += sideToMove == WHITE ? " w " : " b ";
std::string cr;
if (castlingRights & WHITE_OO) cr += 'K';
if (castlingRights & WHITE_OOO) cr += 'Q';
if (castlingRights & BLACK_OO) cr += 'k';
if (castlingRights & BLACK_OOO) cr += 'q';
s += cr.empty() ? "-" : cr;
s += ' ';
if (epSquare == SQ_NONE) s += '-';
else { s += char('a' + file_of(epSquare)); s += char('1' + rank_of(epSquare)); }
s += ' ';
s += std::to_string(rule50);
s += ' ';
s += std::to_string(fullmove_number());
return s;
}
Bitboard Position::attackers_to(Square s, Bitboard occ) const {
return (PawnAttacks[BLACK][s] & pieces(WHITE, PAWN))
| (PawnAttacks[WHITE][s] & pieces(BLACK, PAWN))
| (KnightAttacks[s] & byTypeBB[KNIGHT])
| (KingAttacks[s] & byTypeBB[KING])
| (bishop_attacks(s, occ) & (byTypeBB[BISHOP] | byTypeBB[QUEEN]))
| (rook_attacks(s, occ) & (byTypeBB[ROOK] | byTypeBB[QUEEN]));
}
bool Position::in_check() const {
return (attackers_to(king_square(sideToMove)) & pieces(~sideToMove)) != 0;
}
bool Position::gives_check(Move m) {
do_move(m);
bool checked = in_check();
undo_move(m);
return checked;
}
void Position::do_move(Move m) {
Color us = sideToMove, them = ~us;
Square from = m.from(), to = m.to();
MoveFlag flag = m.type();
Piece pc = board[from];
Piece captured = (flag == EN_PASSANT) ? make_piece(them, PAWN) : board[to];
Undo& u = undoStack[undoCount++];
u.castlingRights = castlingRights;
u.epSquare = epSquare;
u.rule50 = rule50;
u.key = zkey;
u.captured = captured;
if (epSquare != SQ_NONE) {
zkey ^= Zobrist::enpassant[file_of(epSquare)];
epSquare = SQ_NONE;
}
++rule50;
if (captured != NO_PIECE) {
Square capsq = to;
if (flag == EN_PASSANT) capsq = (us == WHITE) ? Square(to - 8) : Square(to + 8);
remove_piece(capsq);
rule50 = 0;
}
move_piece(from, to);
if (type_of(pc) == PAWN) {
rule50 = 0;
if ((int(to) ^ int(from)) == 16) {
epSquare = Square((from + to) / 2);
zkey ^= Zobrist::enpassant[file_of(epSquare)];
} else if (flag == PROMOTION) {
remove_piece(to);
put_piece(make_piece(us, m.promotion()), to);
}
}
if (flag == CASTLING) {
Square rookFrom, rookTo;
if (to > from) { rookFrom = Square(from + 3); rookTo = Square(from + 1); }
else { rookFrom = Square(from - 4); rookTo = Square(from - 1); }
move_piece(rookFrom, rookTo);
}
int cr = castlingRights & ~(castling_mask(from) | castling_mask(to));
if (cr != castlingRights) {
zkey ^= Zobrist::castling[castlingRights];
zkey ^= Zobrist::castling[cr];
castlingRights = cr;
}
sideToMove = them;
zkey ^= Zobrist::side;
++gamePly;
repKeys[repCount++] = zkey;
}
void Position::undo_move(Move m) {
Color us = ~sideToMove;
Square from = m.from(), to = m.to();
MoveFlag flag = m.type();
Undo u = undoStack[--undoCount];
if (flag == PROMOTION) {
remove_piece(to);
put_piece(make_piece(us, PAWN), to);
}
move_piece(to, from);
if (u.captured != NO_PIECE) {
Square capsq = to;
if (flag == EN_PASSANT) capsq = (us == WHITE) ? Square(to - 8) : Square(to + 8);
put_piece(u.captured, capsq);
}
if (flag == CASTLING) {
Square rookFrom, rookTo;
if (to > from) { rookFrom = Square(from + 3); rookTo = Square(from + 1); }
else { rookFrom = Square(from - 4); rookTo = Square(from - 1); }
move_piece(rookTo, rookFrom);
}
sideToMove = us;
castlingRights = u.castlingRights;
epSquare = u.epSquare;
rule50 = u.rule50;
zkey = u.key;
--gamePly;
--repCount;
}
bool Position::insufficient_material() const {
if (byTypeBB[PAWN] | byTypeBB[ROOK] | byTypeBB[QUEEN])
return false;
int minors = popcount(byTypeBB[KNIGHT] | byTypeBB[BISHOP]);
return minors <= 1; // KvK, KvKN, KvKB
}
bool Position::is_draw() const {
if (rule50 >= 100) return true;
if (insufficient_material()) return true;
uint64_t k = repKeys[repCount - 1];
int seen = 0;
for (int i = repCount - 3; i >= 0 && i >= repCount - 1 - rule50; i -= 2)
if (repKeys[i] == k && ++seen >= 2)
return true; // threefold
return false;
}
void Position::print() const {
std::printf("\n +---+---+---+---+---+---+---+---+\n");
for (int r = 7; r >= 0; --r) {
std::printf("%d ", r + 1);
for (int f = 0; f < 8; ++f) {
Piece pc = board[make_square(File(f), Rank(r))];
std::printf("| %c ", pc == NO_PIECE ? ' ' : piece_to_char(pc));
}
std::printf("|\n +---+---+---+---+---+---+---+---+\n");
}
std::printf(" a b c d e f g h\n");
std::printf(" %s to move key=%016llx\n",
sideToMove == WHITE ? "White" : "Black",
(unsigned long long)zkey);
}
} // namespace chess
+87
View File
@@ -0,0 +1,87 @@
// The board. Hybrid representation: bitboards (per piece type and per color)
// for fast generation/attacks, plus a piece-on-square mailbox for O(1)
// "what's here?" queries. do_move/undo_move keep both in sync, along with the
// Zobrist key. One Position is one game line; it is freely copyable.
#ifndef CHESS_POSITION_H
#define CHESS_POSITION_H
#include "types.h"
#include "bitboard.h"
#include <string>
#include <string_view>
namespace chess {
class Position {
public:
/// Parse a FEN string into a position.
static Position from_fen(std::string_view fen);
/// Serialize back to FEN.
std::string to_fen() const;
// --- mailbox queries ---
Piece piece_on(Square s) const { return board[s]; }
bool empty(Square s) const { return board[s] == NO_PIECE; }
Color side_to_move() const { return sideToMove; }
Square ep_square() const { return epSquare; }
int halfmove_clock() const { return rule50; }
int fullmove_number() const { return 1 + gamePly / 2; }
bool can_castle(Color c, CastlingSide side) const;
Square king_square(Color c) const { return lsb(pieces(c, KING)); }
// --- bitboard accessors ---
Bitboard pieces() const { return byColorBB[WHITE] | byColorBB[BLACK]; }
Bitboard pieces(Color c) const { return byColorBB[c]; }
Bitboard pieces(PieceType pt) const { return byTypeBB[pt]; }
Bitboard pieces(Color c, PieceType pt) const { return byTypeBB[pt] & byColorBB[c]; }
// --- attacks / checks ---
Bitboard attackers_to(Square s) const { return attackers_to(s, pieces()); }
Bitboard attackers_to(Square s, Bitboard occ) const;
bool in_check() const; // is side_to_move in check?
bool gives_check(Move m); // does m check the opponent?
// --- the three you asked for ---
void generate_legal(MoveList& list); // defined in movegen.cpp
void do_move(Move m);
void undo_move(Move m);
// --- freebies ---
uint64_t key() const { return zkey; }
bool is_draw() const; // 50-move + threefold + insufficient material
void print() const;
private:
void put_piece(Piece pc, Square s);
void remove_piece(Square s);
void move_piece(Square from, Square to);
bool insufficient_material() const;
Bitboard byTypeBB[PIECE_TYPE_NB];
Bitboard byColorBB[COLOR_NB];
Piece board[SQUARE_NB];
Color sideToMove;
int castlingRights;
Square epSquare;
int rule50;
int gamePly;
uint64_t zkey;
struct Undo {
int castlingRights;
Square epSquare;
int rule50;
uint64_t key;
Piece captured;
};
Undo undoStack[1024];
int undoCount;
uint64_t repKeys[1024];
int repCount;
};
} // namespace chess
#endif // CHESS_POSITION_H
+107
View File
@@ -0,0 +1,107 @@
// Core vocabulary for the chess engine: squares, pieces, moves.
// Everything else is built on these. Convention: A1 = 0 ... H8 = 63,
// file = square & 7 (A..H), rank = square >> 3 (1..8). North = +8.
#ifndef CHESS_TYPES_H
#define CHESS_TYPES_H
#include <cstdint>
namespace chess {
using Bitboard = uint64_t;
enum Color : int { WHITE, BLACK, COLOR_NB = 2 };
enum PieceType : int {
NO_PIECE_TYPE, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING, PIECE_TYPE_NB = 8
};
enum Piece : int {
NO_PIECE,
W_PAWN = PAWN, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING,
B_PAWN = PAWN + 8, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING,
PIECE_NB = 16
};
enum Square : int {
A1, B1, C1, D1, E1, F1, G1, H1,
A2, B2, C2, D2, E2, F2, G2, H2,
A3, B3, C3, D3, E3, F3, G3, H3,
A4, B4, C4, D4, E4, F4, G4, H4,
A5, B5, C5, D5, E5, F5, G5, H5,
A6, B6, C6, D6, E6, F6, G6, H6,
A7, B7, C7, D7, E7, F7, G7, H7,
A8, B8, C8, D8, E8, F8, G8, H8,
SQ_NONE,
SQUARE_NB = 64
};
enum File : int { FILE_A, FILE_B, FILE_C, FILE_D, FILE_E, FILE_F, FILE_G, FILE_H, FILE_NB = 8 };
enum Rank : int { RANK_1, RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8, RANK_NB = 8 };
enum CastlingSide : int { KINGSIDE, QUEENSIDE };
// Castling rights as a bitmask.
enum CastlingRights : int {
NO_CASTLING = 0,
WHITE_OO = 1, WHITE_OOO = 2,
BLACK_OO = 4, BLACK_OOO = 8,
ANY_CASTLING = 15
};
constexpr Color operator~(Color c) { return Color(c ^ BLACK); }
constexpr Square make_square(File f, Rank r) { return Square((r << 3) + f); }
constexpr File file_of(Square s) { return File(s & 7); }
constexpr Rank rank_of(Square s) { return Rank(s >> 3); }
constexpr Piece make_piece(Color c, PieceType pt) { return Piece((c << 3) + pt); }
constexpr PieceType type_of(Piece p) { return PieceType(p & 7); }
constexpr Color color_of(Piece p) { return Color(p >> 3); } // assumes p != NO_PIECE
// A move packed into 16 bits: from:6 | to:6 | promotion:2 | flag:2.
// The promotion bits encode KNIGHT..QUEEN as 0..3 and are only meaningful
// when the flag is PROMOTION.
enum MoveFlag : int { NORMAL, PROMOTION, EN_PASSANT, CASTLING };
struct Move {
uint16_t data;
constexpr Move() : data(0) {}
constexpr explicit Move(uint16_t d) : data(d) {}
/// Build a move. `promo` only matters when `flag == PROMOTION`.
static constexpr Move make(Square from, Square to, MoveFlag flag = NORMAL,
PieceType promo = KNIGHT) {
return Move(uint16_t((flag << 14) | ((promo - KNIGHT) << 12) | (to << 6) | from));
}
constexpr Square from() const { return Square(data & 0x3F); }
constexpr Square to() const { return Square((data >> 6) & 0x3F); }
constexpr MoveFlag type() const { return MoveFlag((data >> 14) & 0x3); }
constexpr PieceType promotion() const { return PieceType(((data >> 12) & 0x3) + KNIGHT); }
constexpr bool operator==(Move m) const { return data == m.data; }
constexpr bool operator!=(Move m) const { return data != m.data; }
};
// A1->A1 is never a real move, so an all-zero move is our "none" sentinel.
constexpr Move MOVE_NONE = Move(0);
// Fixed-capacity, allocation-free, range-for friendly. 256 covers any legal position.
struct MoveList {
Move moves[256];
int count = 0;
void add(Move m) { moves[count++] = m; }
int size() const { return count; }
Move* begin() { return moves; }
Move* end() { return moves + count; }
const Move* begin() const { return moves; }
const Move* end() const { return moves + count; }
};
} // namespace chess
#endif // CHESS_TYPES_H
+50
View File
@@ -0,0 +1,50 @@
#include "uci.h"
#include <cstdlib>
namespace chess {
std::string move_to_uci(Move m) {
if (m == MOVE_NONE) return "0000";
Square f = m.from(), t = m.to();
std::string s;
s += char('a' + file_of(f));
s += char('1' + rank_of(f));
s += char('a' + file_of(t));
s += char('1' + rank_of(t));
if (m.type() == PROMOTION) {
static const char promo[PIECE_TYPE_NB] = { 0, 0, 'n', 'b', 'r', 'q', 0 };
s += promo[m.promotion()];
}
return s;
}
Move move_from_uci(const Position& pos, std::string_view uci) {
if (uci.size() < 4) return MOVE_NONE;
Square from = make_square(File(uci[0] - 'a'), Rank(uci[1] - '1'));
Square to = make_square(File(uci[2] - 'a'), Rank(uci[3] - '1'));
if (uci.size() >= 5) {
PieceType promo = QUEEN;
switch (uci[4]) {
case 'q': promo = QUEEN; break;
case 'r': promo = ROOK; break;
case 'b': promo = BISHOP; break;
case 'n': promo = KNIGHT; break;
}
return Move::make(from, to, PROMOTION, promo);
}
Piece pc = pos.piece_on(from);
if (type_of(pc) == KING && std::abs(int(to) - int(from)) == 2)
return Move::make(from, to, CASTLING);
if (type_of(pc) == PAWN && to == pos.ep_square() && file_of(from) != file_of(to))
return Move::make(from, to, EN_PASSANT);
return Move::make(from, to);
}
} // namespace chess
+19
View File
@@ -0,0 +1,19 @@
// Conversions between moves and UCI long-algebraic strings ("e2e4", "e7e8q").
// move_from_uci resolves the move's flag (castling / en passant / promotion)
// against the given position.
#ifndef CHESS_UCI_H
#define CHESS_UCI_H
#include "position.h"
#include <string>
#include <string_view>
namespace chess {
std::string move_to_uci(Move m);
Move move_from_uci(const Position& pos, std::string_view uci);
} // namespace chess
#endif // CHESS_UCI_H
+39
View File
@@ -0,0 +1,39 @@
#include "zobrist.h"
namespace chess {
namespace Zobrist {
uint64_t psq[PIECE_NB][SQUARE_NB];
uint64_t enpassant[FILE_NB];
uint64_t castling[16];
uint64_t side;
namespace {
struct PRNG {
uint64_t s;
explicit PRNG(uint64_t seed) : s(seed) {}
uint64_t next() {
s ^= s >> 12; s ^= s << 25; s ^= s >> 27;
return s * 2685821657736338717ULL;
}
};
}
void init() {
PRNG rng(0xC0FFEE123456789Aull);
for (int p = 0; p < PIECE_NB; ++p)
for (int s = 0; s < SQUARE_NB; ++s)
psq[p][s] = rng.next();
for (int f = 0; f < FILE_NB; ++f)
enpassant[f] = rng.next();
for (int c = 0; c < 16; ++c)
castling[c] = rng.next();
side = rng.next();
}
} // namespace Zobrist
} // namespace chess
+21
View File
@@ -0,0 +1,21 @@
// Zobrist hashing keys. Filled once by Zobrist::init() (from engine_create).
// Position maintains the running key incrementally in do_move/undo_move.
#ifndef CHESS_ZOBRIST_H
#define CHESS_ZOBRIST_H
#include "types.h"
namespace chess {
namespace Zobrist {
extern uint64_t psq[PIECE_NB][SQUARE_NB];
extern uint64_t enpassant[FILE_NB];
extern uint64_t castling[16];
extern uint64_t side;
void init();
} // namespace Zobrist
} // namespace chess
#endif // CHESS_ZOBRIST_H
+76
View File
@@ -0,0 +1,76 @@
// Standalone perft harness: validates move generation + make/unmake against
// published node counts. Build separately from the DLL (see build instructions
// in the repo); not part of the shipped library.
#include "../src/bitboard.h"
#include "../src/zobrist.h"
#include "../src/position.h"
#include "../src/perft.h"
#include <cstdint>
#include <cstdio>
using namespace chess;
struct Case {
const char* name;
const char* fen;
int depth;
uint64_t expected;
};
// Verifies the incrementally-maintained key matches a from-scratch hash of the
// same position (also exercises to_fen -> from_fen round-tripping).
static uint64_t verify_keys(Position& pos, int depth) {
uint64_t mismatches = 0;
if (pos.key() != Position::from_fen(pos.to_fen()).key())
++mismatches;
if (depth == 0) return mismatches;
MoveList list;
pos.generate_legal(list);
for (Move m : list) {
pos.do_move(m);
mismatches += verify_keys(pos, depth - 1);
pos.undo_move(m);
}
return mismatches;
}
int main() {
init_bitboards();
Zobrist::init();
const Case cases[] = {
{"startpos d5", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 5, 4865609ULL},
{"kiwipete d4", "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", 4, 4085603ULL},
{"position3 d5", "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", 5, 674624ULL},
{"position4 d4", "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", 4, 422333ULL},
{"position5 d4", "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", 4, 2103487ULL},
{"position6 d4", "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", 4, 3894594ULL},
};
int fails = 0;
for (const Case& c : cases) {
Position pos = Position::from_fen(c.fen);
uint64_t got = perft(pos, c.depth);
bool ok = (got == c.expected);
std::printf("%-14s %14llu expected %14llu %s\n",
c.name, (unsigned long long)got, (unsigned long long)c.expected,
ok ? "OK" : "FAIL");
if (!ok) ++fails;
}
std::printf("\n%s\n", fails ? "*** PERFT FAILED ***" : "ALL PERFT PASSED");
// Zobrist key + FEN round-trip consistency.
const char* startfen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
const char* kiwifen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
Position a = Position::from_fen(startfen);
Position b = Position::from_fen(kiwifen);
uint64_t km = verify_keys(a, 4) + verify_keys(b, 3);
std::printf("key/fen mismatches: %llu %s\n", (unsigned long long)km,
km == 0 ? "OK" : "FAIL");
if (km) ++fails;
return fails ? 1 : 0;
}