Since the release of Version 1.0 everything I touched was eventually reverted. So I'm still at Version 1.0 despite having spent at least 10h on the engine. As all this work will not show up anywhere else (certainly not in the master branch) I thought I would at least make a quick post about it here.
Deadend #1
I've received some criticism regarding my choice of going copy/make instead of make/unmake. To make the copy step fast I skip storing redundant information as much as possible. Because the placement of all pieces is encoded in the bitboards I don't
have to maintain a mailbox array. 64bytes less data to copy! But... the drawback is that the GetPiece(square) function looks like this.
Code: Select all
public Piece GetPiece(int square)
{
/*
2nd Bit = White or Black? (implies the Piece bit to be set to)
Black = 1, //01
White = 3, //11
3rd+ Bits = Type of Piece
Pawn = 4, //00100
Knight = 8, //01000
Bishop = 12, //01100
Rook = 16, //10000
Queen = 20, //10100
King = 24, //11000
*/
return (Piece)(Bit(Black | White, square, 0) |
Bit(White, square, 1) |
Bit(Pawns | Bishops | Queens, square, 2) |
Bit(Knights | Bishops | Kings, square, 3) |
Bit(Kings | Rooks | Queens, square, 4));
}
private int Bit(ulong bb, int square, int shift) => (int)((bb >> square) & 1) << shift;
And I tried different alternative implementations to speed this function up. There are a few interesting ideas and more details of what I tried to be
found in this thread.
In the end the difference was hard to measure in time-to-depth even and that means there's no trace of a difference to be expected in actual play. And so I shelved the alternatives for now. Because... let's face it... the best way to fetch a piece by square would be the mailbox array. I'll just defer the decision for now and if reasons to forgo a copy/make approach in favor of make/unmake keep accumulating that's exactly what I'm going to do at some later point.
Deadend #2
Leorik 1.0 is just using tapered PSTs and Mike Sherwin showed a great example earlier in the thread what the drawbacks of that are.
In MinimalChess I went one step further by adding a 13th table to the 12 PSTs. For each piece on the board you'd accumulate small values from this table based on what squares that piece could "see". The table has 13 columns and 6 rows. So for each of the 6 pieces there are 13 values: 1 for empty squares, 6 friendly pieces and 6 enemy pieces. These values are auto-tuned. And they can encode a lot of basic chess knowledge:
There's a 2 cp bonus for each empty squares for rooks and bishops. This encourages mobility. There's a 10cp bonus for pawn guarding friendly pawns. This improves the pawn structure. Pieces get a bonus for threatening more valuable pieces. Queens and Knights are encouraged to go hunt for the King.
I ported that feature over to Leorik but sadly it tanks the performance of the engine so much that, in actual self play, the addition isn't beneficial at all. Time to depth increases by 3x!
It was a slow feature in MinimalChess, too, but it that was already a slow engine so it didn't hurt the time to depth as much. And despite the costly computations involved it turned out to be worth over 60 ELO!
MinimalChess used a mailbox representation, though, and that makes it easier to implement this feature. Basically what you have to do is you generate the move targets for *all* pieces and then you need to lookup the peace on *each* targeted square because that determines the index into the 13th table. Implementing that on pure bitboards was not only coming down to a complete rewrite of the code but also the new code is slower due to the lack of a cheap way to lookup pieces on squares.
So as it stands now my plans for the next major release, Leorik 2.0, do not include this evaluation feature. I will focus on the search features but stick with the same evaluation that Leorik 1.0 used. Does that mean Leorik 2.0 is doomed to be still weaker than the latest MinimalChess version is? I don't hope so, hopefully it's speed compensates for the lack of the 13th table. It will be an interesting matchup! Maybe Graham will agree to pit them both against each other in the Amateur Series Division 8.
Deadend #3
I'm never really sure whether I should consider Qsearch to be part of the evaluation or part of the search. It's main purpose is to eliminate the horizon effect so that the evaluation is actually reliable. So you have to resolve all open beneficial captures, that's the main purpose.
But when in check you need to prove that you can escape and can't just use standpat. But if I can't escape, does that really mean checkmate is the correct score to return? E.g. should the qsearch() on a position where side to move is
not in check be allowed to return a checkmate score? I feel like it shouldn't but at the same time it seems unavoidable that it does: While playing captures the king may end up in check and if you can't escape it, you have to return checkmate... what else can you do? But the position is probably not guaranteed to be a lost one, it just seemed so because you were greatly limiting the moves you allowed playing during qsearch.
A sub-problem of that is whether Qsearch should detect and return stalemates. And I just tested both variants now. Turns out not considering stalemate is stronger in selfplay than the alternative where you return if the side to move is not in check but can't play any legal moves (quiet or captures).
I gained knowledge from that test I guess but as this was already what Leorik 1.0 was doing there's nothing to commit to master, again.
---
I feel like it's time to do something that's actually going to boost the engine's strength, next. So I'm going to implement Null Move Pruning and look forward to celebrating 150 ELO gains from it.
