Skip to content

Latest commit

 

History

History
173 lines (136 loc) · 8.79 KB

design.md

File metadata and controls

173 lines (136 loc) · 8.79 KB

Project structure

  • bench contains benchmarks. Each subdirectory corresponds to benchmark for a separate module
  • cmake contains CMake scripts which are useful to build the project
    • cmake/scripts contains executable CMake scripts. These ones are designed to be run with cmake -P command and are used while building the project
    • cmake/overrides contains CMake override files to use in various build modes. Currently, files from here are used in CI builds
  • docs contains documentation for the project and its various utilities
  • gen contains generators for autogenerated files. Each subdirectory corresponds to generators for a specific module
  • selftest contains a testing framework to check the correctness of the rules implementation
  • src contains various directories, each of them represents a module. See below on module structure
    • src/config.h.in contains configuration flags for the entire project
  • third-party contains third-party libraries used in the project. Each library is contained in a separate folder
  • tools contains some useful scripts

Module structure

  • src/<module_name>/ is the root of the module. It contains the public API headers and the implementation of the methods declared there
    • src/<module_name>/private contains private API and its implementation
    • src/<module_name>/test can contain unit tests and other stuff for testing

Each module has its own namespace. E.g. core has namespace SoFCore. For private API and testing routines, there are sub-namespaces, Private and Test respectively.

Public API and private API

Each module can have its private API and public API. The latter is used by other modules, and the former is entirely internal. As mentioned above, private APIs live under the separate Private namespace and their implementation is located in private/ subdirectory.

The following rules apply:

  • public headers must not depend on private headers. Though, implementation of public functionality may depend on private API
  • any module should not use private API of another module
  • public API should be carefully documented (see below), while private API may be left undocumented

Modules

  • util implements helper utilities. It must not depend on any other module
  • core is a fast implementation of basic chess rules
  • bot_api contains APIs for communication between the engine and the GUI. The implementation of these APIs is located in other modules
    • bot_api/clients contains various implementations of engine communication protocols. This module is useful for developing chess engines. It uses the interfaces described in bot_api module. Currently, only UCI is implemented, but support for more protocols will be added later
  • eval contains the implementation of position cost evaluation
    • eval/feat allows to tune the position cost weights. It contains utilities to extract the features from positions to perform such tuning, and to apply the updated weights to the engine
  • gameset contains APIs to work with SoFGameSet format
  • search contains the chess engine implementation. It uses the interface described in bot_api
    • search/bin contains the chess engine executable
  • version is a tiny module that contains the version info. This information is taken from Git at build time. It is designed as a separate module to make recompilation faster, as the version info may change fast (on every commit)
  • more modules coming soon...

Main design principles

  • public APIs must be clear and understandable. Private APIs and implementations are allowed to use any dirty hacks and even depend on some details of the module's public API (e.g. the order of constants in enums, the order of fields in struct, the value of public constants). But it's disregarded when the implementation depends on such details of another module
    • the steps above may make the public API inflexible to minor changes, so be careful
  • performance is better than clean implementation code. If it's necessary to overcome the linter warnings to write faster code, then it can be done
    • the code is optimized primarily for modern x86_64 systems. For them, CPUs are required to have SSE4.2 and POPCNT and encouraged to have BMI2. The code should not throw compile errors on other platforms, though I'm not planning to focus on optimizing it for such targets
    • to detect platform features, use flags in CMake and detect in CMakeLists.txt if this feature is available
    • benchmarks, profilers and assembly listings (with gcc -S or other tools) are encouraged to find a faster solution
    • though it's not a very good idea to optimize a function which is called rarely and doesn't affect timings much

Testing

I don't have any policy for testing, so any approach can be used.

The chess rules are tested against other implementation in selftest directory. This includes:

  • the same behaviour between implementations
  • consistency check (e. g. that the board is updated, and the moves are valid)
  • checks between different functions (e. g. that isMoveLegal returns true if and only if this move is generated by genAllMove)

Use of C++ features

The project uses C++17. Below there is a comment about specific C++ features.

  • constexpr, inline methods and templates help produce faster code, so use them!
  • exceptions are not allowed
  • class inheritance is not allowed in general, with some exceptions
    • you can only inherit from an abstract class without any fields (not all of the methods them must be abstract, some can have a default implementation)

Improvements

When making substantial changes to the engine, I need to ensure that they won't make the engine play worse. So, matches between new and old versions of the engine are conducted before committing the changes into master branch. Currently, BattleField is used to conduct such matches. The acceptance criteria are as follows:

  • small changes which supposedly don't affect playing strength can be accepted without testing
  • if changes make the engine play better, they are accepted
  • if changes don't make the engine play worse and are good in some aspect (nicer behaviour in certain rare positions, various refactorings, and so on), they are accepted
  • otherwise, the changes are not accepted, and we must either debug it or just don't apply

There are also tests that ensure that the engine doesn't break (see above). Though, it's only a basic check and passing tests doesn't guarantee that there are no subtle bugs that descrease playing strength significantly.

Why name "SoFCheck"?

  • SoFCheck => совчик => small owl :)
  • "check" part has relationship to chess
  • SoF ::= Strategy of Figurines (though I'm not sure whether SoF part even has any meaning, it's just an attempt to give any meaning to this part)

Code style

The project uses Gepardo code style. It's much like Java code style, with 2 space wide indentations. To follow the code style, clang-format is configured for the project. clang-tidy will help you to avoid the style errors related to naming convention. Here are the rules:

Naming convention

  • methods and variables are camelCase
  • types and namespaces are BigCamelCase
  • aliases for primitives are snake_case_t
  • enum members are BigCamelCase
  • constants are ANGRY_SNAKE_CASE
  • file names are snake_case.cpp (or snake_case.h)
  • global variables are g_camelCase
  • private members are snakeCase_
  • defines are ANGRY_SNAKE_CASE

On naming non-class functions

  • to create a value make...(). E. g.: makeCell()
  • prefer the form <objectName><action> instead of <action><objectName>. E. g.: moveMake() instead of makeMove()
  • for function returning booleans, you can use the form is<objectName><action>. E. g.: isMoveValid()

Comments

Comments start from uppercase letters. Note that the dot in the end of the comment can be omitted.

It's required to document all the methods in the public API, if it's not entirely clear from their signature what they do. There is no strict form of how this documentation must be written. Note that the following things must be taken into account when writing documentation:

  • behaviour in exceptional cases or invalid data: is it a panic, error code or the behaviour is undefined? By default, it's supposed that you will pass valid parameters into functions, but better be explicit
  • which parameter values considered valid for a function, and which ones considered invalid
  • meaning of the parameters and return values, if it's not clear

Other notes

  • please, use full paths (starting from src) for header files. For example, the header in src/core/board.h must be included as #include "core/board.h".
  • naming of defines in header guards also has strict rules, which I'm too lazy to describe. Anyway, tools/fix_header_guards.py will fix them if necessary.