Skip to content

Commit

Permalink
fixed two major bugs, added utility code
Browse files Browse the repository at this point in the history
added save/load for transposition table,
fixed cutoffs when transposing to upper/lower bounds
fixed late move reductions
changed depth of outcome evals in transposition table
added run_search.cpp
  • Loading branch information
Nonlinear2 committed Jun 9, 2024
1 parent 84b45a5 commit f4bc4e5
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,9 @@ bread_configure(parameter_tuning SOURCES
bread_configure(generate_neural_net_header SOURCES
${bread_SOURCE}
${bread_UTILS}/generate_nn_header.cpp
)

bread_configure(search_position SOURCES
${bread_SOURCE}
${bread_UTILS}/run_search.cpp
)
4 changes: 4 additions & 0 deletions dependencies/transposition_table.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "chess.hpp"
#include <vector>
#include <fstream>

enum class TFlag: std::uint8_t {
NO_FLAG,
Expand Down Expand Up @@ -40,6 +41,9 @@ class TranspositionTable {

int hashfull();

void save_to_file(std::string file);
void load_from_file(std::string file);

private:
std::vector<TEntry> entries;
int size_mb;
Expand Down
6 changes: 4 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Besides the main engine build, you can also build some utility code to:
- tune some engine parameters
- run tests
- benchmark the engine
- run a search for a given fen

These can all be built using cmake.

# Technical details
Expand All @@ -47,7 +49,7 @@ in this section we provide an overview of the search algorithm, and the main eng
A chess engine is only just a program that takes a chess position and returns the corresponding best move to play. To play a full game of chess, you can just give the engine the positions that arise on the board one after the other.

### Minimax
Quantifiying what a "good move" is in chess is tricky, it is easier to tell who has the advantage on a given position.
A "good move" in chess can be defined as a move which "improves your position".

So let's say you wrote a function that takes a chess position and returns a score that is positive if white has the advantage, and negative if black has the advantage. A very basic implementation would be to count the white pieces, and subtract the number of black pieces.
The associated score is usually in centipawns, but the measure is arbitrary, the important is that the evaluation is coherent: if the position A is better than B for white, then f(A) > f(B).
Expand Down Expand Up @@ -194,7 +196,7 @@ void HiddenLayer<in_size, out_size>::run(int8_t* input, int32_t* output){
# Notable games
nnue without quantization vs nnue with quantization 0-1:
1. d4 Nf6 2. c4 c5 3. d5 e6 4. Nc3 exd5 5. cxd5 d6 6. e4 g6 7. Bf4 a6 8. Bd3 b5 9. Nge2 Bg7 10. a3 O-O 11. O-O Nh5 12. Bc1 Nd7 13. h3 Ne5 14. Bc2 Nf3+ 15. gxf3 Bxh3 16. Ng3 Qh4 17. Nce2 Rae8 18. Re1 Bd4 19. Nxd4 Nxg3 20. Ne2 Nxe2+ 21. Qxe2 f5 22. Bd2 f4 23. Bxf4 Rxf4 24. Kh2 Bf5+ 25. Kg2 Qg5+ 26. Kf1 Bh3# 0-1
1\. d4 Nf6 2. c4 c5 3. d5 e6 4. Nc3 exd5 5. cxd5 d6 6. e4 g6 7. Bf4 a6 8. Bd3 b5 9. Nge2 Bg7 10. a3 O-O 11. O-O Nh5 12. Bc1 Nd7 13. h3 Ne5 14. Bc2 Nf3+ 15. gxf3 Bxh3 16. Ng3 Qh4 17. Nce2 Rae8 18. Re1 Bd4 19. Nxd4 Nxg3 20. Ne2 Nxe2+ 21. Qxe2 f5 22. Bd2 f4 23. Bxf4 Rxf4 24. Kh2 Bf5+ 25. Kg2 Qg5+ 26. Kf1 Bh3# 0-1
# Acknowledgements
[lichess's open database](https://database.lichess.org/) for training data for the neural network.
Expand Down
25 changes: 14 additions & 11 deletions src/Bread_engine_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ std::pair<std::string, std::string> Engine::get_pv_pmove(std::string fen){
for (int i = 0; i < search_depth; i++){
bool is_hit;
TEntry* transposition = transposition_table.probe(is_hit, pv_visitor.hash());
if (!is_hit || transposition->best_move == NO_MOVE){
if ((!is_hit) || (transposition->best_move == NO_MOVE)){
break;
}
if (i == 1){
Expand Down Expand Up @@ -341,6 +341,10 @@ float Engine::negamax(int depth, int color, float alpha, float beta){
beta = std::min(beta, transposition->evaluation);
break;
}
// we need to do a cutoff check because we updated alpha/beta.
if (beta <= alpha){
return transposition->evaluation;
}
}

if (depth == 0){
Expand All @@ -358,10 +362,10 @@ float Engine::negamax(int depth, int color, float alpha, float beta){
if (legal_moves.empty()){ // avoid calling expensive try_outcome_eval function
max_eval = get_outcome_eval(depth);

// what depth to store is hard to choose, as we know this eval will be exact at any depth, but
// we also don't want this eval to pollute the transposition table. We want it replaced at some point.
// this is why we stick to the "depth".
transposition_table.store(zobrist_hash, max_eval, depth, NO_MOVE, TFlag::EXACT, static_cast<uint8_t>(inner_board.fullMoveNumber()));
// we know this eval is exact at any depth, but
// we also don't want this eval to pollute the transposition table.
// the full move number will make sure it is replaced at some point.
transposition_table.store(zobrist_hash, max_eval, 255, NO_MOVE, TFlag::EXACT, static_cast<uint8_t>(inner_board.fullMoveNumber()));
return max_eval;
}

Expand All @@ -374,14 +378,14 @@ float Engine::negamax(int depth, int color, float alpha, float beta){
for (int i = 0; i < legal_moves.size(); i++){
move = legal_moves[i];

bool is_capture = inner_board.isCapture(move);
inner_board.update_state(move);

// depth > 2 is to make sure the new depth is not less than 0.
// (!inner_board.inCheck()) is to see if the move gives check.
// (we already updated the inner board so we only need to check if it is check)
if ((i > 3) & (depth > 2) & (inner_board.at(move.to()) == chess::Piece::NONE)
& (!inner_board.inCheck())){
new_depth--;
if ((i > 3) & (depth > 2) & (!is_capture) & (!inner_board.inCheck())){
new_depth = depth-2;
lmr = true;
}

Expand Down Expand Up @@ -452,7 +456,7 @@ float Engine::qsearch(float alpha, float beta, int color, int depth){
bool is_hit;
TEntry* transposition = transposition_table.probe(is_hit, zobrist_hash);
if (is_hit){
if (transposition->depth == 0){ // careful, we use the fact that outcomes are stored at depth 3 here.
if (transposition->depth == 0){ // careful, we use the fact that outcomes are stored at depth 255 here.
stand_pat = transposition->evaluation;

// we can already check for cutoff
Expand Down Expand Up @@ -481,8 +485,7 @@ float Engine::qsearch(float alpha, float beta, int color, int depth){
if (legal_captures.empty()){
float outcome_eval;
if (try_outcome_eval(outcome_eval)){ // only generate non captures?
transposition_table.store(zobrist_hash, outcome_eval, 3, NO_MOVE, TFlag::EXACT, static_cast<uint8_t>(inner_board.fullMoveNumber()));
// higher depth stored because is exact at any depth.
transposition_table.store(zobrist_hash, outcome_eval, 255, NO_MOVE, TFlag::EXACT, static_cast<uint8_t>(inner_board.fullMoveNumber()));
return outcome_eval;
}
}
Expand Down
29 changes: 28 additions & 1 deletion src/transposition_table.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,31 @@ int TranspositionTable::hashfull(){
used += (entries[i].zobrist_hash != 0);
}
return used;
}
}

void TranspositionTable::save_to_file(std::string file){
std::ofstream ofs = std::ofstream(file, std::ios::binary | std::ios::out);
if (!ofs) {
std::cout << "Could not open file for writing." << std::endl;
return;
}

for (const auto& entry : entries) {
ofs.write(reinterpret_cast<const char*>(&entry), sizeof(TEntry));
}

ofs.close();
}


void TranspositionTable::load_from_file(std::string file){
std::ifstream ifs(file, std::ios::binary | std::ios::in);
if (!ifs) {
std::cout << "Could not open file for reading." << std::endl;
}
for (size_t i = 0; i < entries.size(); ++i) {
ifs.read(reinterpret_cast<char*>(&entries[i]), sizeof(TEntry));
}

ifs.close();
}
19 changes: 19 additions & 0 deletions utils/run_search.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include "bread_engine_core.hpp"
#include <iostream>
int main(){
Engine engine = Engine();
std::vector<std::string> fens = {
"r2qkbnr/1pp2ppp/p1p5/4p3/4P1b1/5N1P/PPPP1PP1/RNBQ1RK1 b kq - 0 6",
};

for (int i = 0; i < fens.size(); i++){
chess::Move best;
std::cout << fens[i] << "\n";
best = engine.search(fens[i], 13878, 7, 7);
std::cout << "best move " << best << "\n";
std::cout << "eval " << best.score() << std::endl;
std::cout << engine.search_depth << std::endl;
engine.transposition_table.info();
}
return 0;
}

0 comments on commit f4bc4e5

Please sign in to comment.