emadsen wrote: ↑Wed Mar 17, 2021 1:56 am
Running gauntlet tournaments is a great arbiter of the truth. It doesn't inform you of the proper coding technique, proper algorithm, etc. But it's really good at informing you when your code change is a regression, lol.
I do run gauntlets (several actual, for this change), and the improvement was +105 Elo; so the TT did work.
However, because of the poor performance of the TT move ordering since the beginning, I had a nagging problem that I was doing something wrong with saving the "best move" to the TT, but didn't know why, because in EVERY explanation I found, after endless googling, was that the "best move" should be saved.
The problem was that the "best move" is open to discussion... is it the "best move" in the move loop so far? Is it the move that last raised alpha? (I took it as such, because Alpha is where you collect the PV), is it the move that causes a beta-cutoff... it actually is the very first: the move with the highest eval_score in the loop, not the one that last raised alpha.
You really have to internalize the ideas. Just as restating ideas in your own words is valuable when learning a new topic, so is restating chess programming techniques in your own code.
That is precisely what I'm doing, and the reason why the development of my engine is so slow. I'm writing everything from scratch, reading only pseudo-code (and I absolutely avoid old C code, because those engines have LOTS of things in the global space.) Because I really dislike massively nested IF-statements, I now have this, and I believe this is correct:
Code: Select all
// eval_score is better than the best we found so far, so we
// save a new best_move that'll go into the hash table.
if eval_score > best_eval_score {
best_eval_score = eval_score;
best_move = current_move.to_hash_move();
}
// Beta cutoff: this move is so good for our opponent, that we
// do not search any further. Insert into TT and return beta.
if eval_score >= beta {
refs.tt.lock().expect(ErrFatal::LOCK).insert(
refs.board.game_state.zobrist_key,
SearchData::create(
depth,
refs.search_info.ply,
HashFlag::BETA,
beta,
best_move,
),
);
return beta;
}
// We found a better move for us.
if eval_score > alpha {
// Save our better evaluation score as alpha.
alpha = eval_score;
// This is an exact move score.
hash_flag = HashFlag::EXACT;
// Update the Principal Variation.
pv.clear();
pv.push(current_move);
pv.append(&mut node_pv);
}
... after the loop we insert the best_move, either as flag:ALPHA or flag::EXACT ...
It should be the "untangled" version of this:
Code: Select all
// eval_score is better than the best we found so far, so we
// save a new best_move that'll go into the hash table.
if eval_score > best_eval_score {
best_eval_score = eval_score;
best_move = current_move.to_hash_move();
// We found a better move...
if eval_score > alpha {
// ... for our opponent.
if eval_score >= beta {
refs.tt.lock().expect(ErrFatal::LOCK).insert(
refs.board.game_state.zobrist_key,
SearchData::create(
depth,
refs.search_info.ply,
HashFlag::BETA,
beta,
best_move,
),
);
return beta;
}
// ... for us.
alpha = eval_score;
// This is an exact move score.
hash_flag = HashFlag::EXACT;
// Update the Principal Variation.
pv.clear();
pv.push(current_move);
pv.append(&mut node_pv);
}
}
... below the loop we save the best_move as ALPHA or EXACT ...
This may be nitpicking, but I get REALLY confused if there are too many if/else statements (or loops/switch/match) nested into one another. I dislike it with a passion, so I try to prevent it, even if it takes a tiny bit of performance by having to evaluate more if-statements. (The above code is still doable, but yeah... principles.)
Jargon is tossed around in forum posts with imprecise language, people may use the same term to mean slightly different things, etc. On the negative side, this will not be the last you experience a bug that weakens engine strength without causing any outright failures. On the positive side, you'll gain ELO from this code change. And you'll develop a healthy skepticism of your own code. I wince when someone says, "There are no bugs in my code, this must be an issue that benefits more mature engines..." I don't really take in the second half of such a sentence. Can't get past the dangerous optimism in the first half.
The mixup of jargon and terminology is often most confusing.
I hope I was just in time to retract the release and nobody downloaded it already, or I _WOULD_ have had a massive bug in my engine; one I suspected since I finalized the TT, but couldn't put my finger on. That's why the release took so long, and I even posted those two topics AFTER releasing the engine because even after, i couldn't convince myself that everything was 100% correct. I just felt the TT should have given me more benefit, and as far as I can see, it now will.
"I have no bugs in my code" is something I normally don't dare to say; but "my code probably has a lot less bugs than average" is something I'm convinced of. Development of my code is also often much slower, and tested for far longer, than what I see on average. You learn to work that way, if you're doing embedded software and control systems, and (nearly) seriously damaged a €100.000 piece of machinery by making a mistake. Been there, done that; fortunately only once. It was a heart-stopping moment though, seeing a conveyor belt running a pallet into a packaging machine at something like 50 km/h or so