Transposition tables are hard...

Discussion of chess software programming and technical issues.

Moderator: Ras

eligolf
Posts: 114
Joined: Sat Nov 14, 2020 12:49 pm
Full name: Elias Nilsson

Transposition tables are hard...

Post by eligolf »

Hi,

Transposition tables are very frustrating, I think I understand what is going on but then engine does something crazy :)
I first implemented it according to Bruce's website and following along Maksims Youtube tutorials. Then I changed to an approach similar to Leorik and Cosette for the store/read but kept the search store/read places as before.

All seems fine and it plays as before implementing the tables, but with a reduced node count of around half. All good I thought.. Then I got to the endgame and saw some really strange behaviors, when it was finding/not finding mates which makes me think something is wrong with how I store these scores. Since I followed all tutorials and have checked the code for a around a week without finding what can be wrong I am now hoping someone here is able to point me in the right direction.

Given this position it finds the mate in 4 with Bc4 at depth 8 and onwards.
[fen]k7/4P3/3B4/8/1p2p3/pP2P1P1/P1P1B1P1/4K3 w - - 0 1[/fen]


The moves in PV looks a bit strange though and it looks like it thinks white plays twice in a row:

Code: Select all

 1	00:00	 24	8	+13,69	e7-e8Q+
 2	00:00	 103	8	+14,13	e7-e8Q+ Ka8-b7
 3	00:00	 318	8	+14,13	e7-e8Q+ Ka8-b7 Bd6xb4
 4	00:00	 560	560k	+18,53	Bd6xb4 Ka8-b8 e7-e8Q+ Kb8-b7
 5	00:00	 3k	481k	+15,83	Be2-a6 Ka8-a7 Bd6xb4 Ka7xa6 e7-e8Q
 6	00:00	 9k	429k	+18,63	Be2-c4 Ka8-a7 e7-e8Q Ka7-b7 Bd6xb4 Kb7-a7
 7	00:00	 56k	607k	+18,90	Bd6xb4 Ka8-b8 e7-e8Q+ Kb8-a7 Bb4xa3 Ka7-b7
 8	00:00	 148k	449k	+M4	Be2-c4 Ka8-b7 e7-e8Q Kb7-b6 Kb6-c6
 9	00:02	 1 441k	643k	+M4	Be2-c4 Ka8-b7 e7-e8Q Kb7-b6 Kb6-a5
 10	00:03	 1 823k	574k	+M4	Be2-c4 Ka8-a7 e7-e8Q Ka7-b7 Kb7-c6
 11	00:07	 4 631k	629k	+M4	Be2-c4 Ka8-a7 e7-e8Q Ka7-b7 Kb7-c6

Then if I play Ka7 it suddenly forgets the mate and print a completely nonsense PV line for the depths:

Code: Select all

 1	00:00	 24	8	+13,59	e7-e8Q
 2	00:00	 113	8	+18,05	e7-e8Q Ka7-b7
 3	00:00	 379	190k	+18,05	e7-e8Q Ka7-b7 Bd6xb4
 4	00:00	 1k	274k	+18,59	Bc4-d5 Ka7-a6 e7-e8Q Ka6-a7
 5	00:00	 4k	366k	+18,63	e7-e8Q Ka7-b7 Bd6xb4 Kb7-a7 Bb4xa3
 6	00:00	 12k	349k	+16,13	Bc4-a6 Ka7xa6 e7-e8Q Ka6-a5 Ka5-b5
 7	00:00	 37k	410k	+M3	e7-e8Q Ka7-b7 Kb7-c6
 8	00:00	 60k	409k	+19,33	Bd6xb4 Ka7-b7 Bb4xa3 Kb7-b8 e7-e8Q+ Kb8-a7 Bc4-f7 Ka7-b7
 9	00:00	 163k	488k	+19,50	Bd6xb4 Ka7-a8 e7-e8Q+ Ka8-b7 Bb4xa3 Kb7-b6 Kb6-a7 e4xe8
 10	00:00	 316k	435k	+20,01	Bd6xb4 Ka7-b8 Bb4xa3 Kb8-a8 e7-e8Q+ Ka8-a7 Bc4-d5 Ka7-b6 Ba3-d6 Kb6-a7
 11	00:01	 688k	494k	+M5	Bd6xb4 Ka7-a8 Bb4-d6 Ka8-a7 e7-e8Q Ka7-b7 Kb7-a8 Bc4-d5+
I read the TT at the beginning after finding if it is draw. Then I store it in 3 places; when depth is 0 with flag Exact, when beta cutoff and at the end before returning, see code below.

NEGAMAX code (with some storing and other things cleaned for simplicity of reading:

Code: Select all

private int Negamax(int depth, int ply, int alpha, int beta)
        {
            // Find if it is a draw
            if (boardState.CheckForDraw()) return 0;

            // Init variables
            int score;
            bool foundPV = false;
            bool isPV = (beta - alpha) > 1;

            // Check transpositions (not in PV nodes, not when ply == 0 and not when we don't have an entry)
            if (Transpositions.ReadTT(boardState.ZobristKey, depth, ply, alpha, beta, out Move _bestMove, out int _score))
            {
                if (!isPV && ply != 0)
                {
                    return _score;
                }
            }

            // Check if we reached depth 0 and go into Quiscence search
            if (depth == 0)
            {
                score = QuiescenceSearch(ply, alpha, beta);
                Transpositions.WriteTT(boardState.ZobristKey, depth, ply, alpha, beta, score, bestMove);
                return score;
            }
            
            // Init PV capturing tables
            pvTable[ply][ply] = new Move();
            pvLength[ply] = 0;

            // Check if we are in check
            bool isInCheck = boardState.IsSquareAttacked(boardState.ColorToMove, ownKingIndex);            

            // Get legal moves
            Move[] children = boardState.GetAllMoves();
            SortMoves(ply, children);
            
            // Init number of legal moves and moves searched
            int legalMoves = 0;

            // Negamax loop
            foreach (Move move in children)
            {
                // Break if finding illegal move
                if (move == new Move()) break;

                // Try if move is legal
                if (boardState.MakeMove(move))
                {
                    // Increase legal moves and node count
                    legalMoves++;

                    // Search and unmake move
                    score = -Negamax(depth - 1, ply + 1, -beta, -alpha);
                    boardState.UnmakeMove(move);

                    // Found a better move
                    if (score > alpha)
                    {
                        foundPV = true;
                        alpha = score;

                        // Write PV move to PV table for the given ply
                        pvTable[ply][ply] = move;

                        // Loop over the next ply
                        for (int j = 0; j < pvLength[ply + 1]; j++)
                        {
                            // Copy move from deeper ply into current ply's line
                            pvTable[ply][ply + 1 + j] = pvTable[ply + 1][ply + 1 + j];
                        }

                        // Adjust PV lenght
                        pvLength[ply] = 1 + pvLength[ply + 1];

                        // Fail hard beta cutoff (node fails high)
                        if (score >= beta)
                        {
                            // Store transposition tabel entry
                            Transpositions.WriteTT(boardState.ZobristKey, depth, ply, alpha, beta, score, bestMove); ;

                            return beta;
                        }
                    }
                }                
            }

            // If we don't have any legal moves in the position, check if it is mate or stalemate
            if (legalMoves == 0)
            {
                // If checkmate, return the mate value, else stalemate value
                if (isInCheck) return -SearchConstants.mateValue + ply;
                else return 0;
            }

            // Write to transposition table wih score = alpha
            Transpositions.WriteTT(boardState.ZobristKey, depth, ply, alpha, beta, alpha, bestMove);

            // Node fails low
            return alpha;
        }
Here is code for read and write to TT, right now it is taken from Leorik straight off, I will change this when I understand what is wrong with the implementation I have :)

Code: Select all

public static bool ReadTT(ulong zobristHash, int depth, int ply, int alpha, int beta, out Move bestMove, out int score)
        {
            bestMove = default;
            score = 0;
            if (!Index(zobristHash, out int index))
                return false;

            ref HashEntry entry = ref hashTable[index];
            bestMove = entry.BestMove;

            if (entry.Depth < depth)
                return false;

            score = AdjustMateDistance(entry.Score, -ply);

            //1.) score is exact and within window
            if (entry.Flag == (byte)Flag.Exact)
                return true;
            //2.) score is below floor
            if (entry.Flag == (byte)Flag.Alpha && score <= alpha)
                return true; //failLow
            //3.) score is above ceiling
            if (entry.Flag == (byte)Flag.Beta && score >= beta)
                return true; //failHigh

            return false;           
        }       

Code: Select all

public static void WriteTT(ulong zobristHash, int depth, int ply, int alpha, int beta, int score, Move bestMove)
        {
            ref HashEntry entry = ref hashTable[Index(zobristHash)];

            // Don't overwrite a bestmove with 'default' unless it's a new position
            if (entry.Key != zobristHash || bestMove != default)
                entry.BestMove = bestMove;

            entry.Key = zobristHash;
            entry.Depth = depth < 0 ? default : (byte)depth;
            entry.Age = 0;

            if (score >= beta)
            {
                entry.Flag = (byte)Flag.Beta;
                entry.Score = AdjustMateDistance(beta, ply);
            }
            else if (score <= alpha)
            {
                entry.Flag = (byte)Flag.Alpha;
                entry.Score = AdjustMateDistance(alpha, ply);
            }
            else
            {
                entry.Flag = (byte)Flag.Exact;
                entry.Score = AdjustMateDistance(score, ply);
            }
        }

Code: Select all

        public static short AdjustMateDistance(int score, int ply)
        {
            if (Math.Abs(score) > SearchConstants.mateScore)
                return (short)(score + Math.Sign(score) * ply);
            else
                return (short)score;
        }
Sorry for all the code, I hope I am missing something obvious in where I read or write to the TT and the input values I use there. It shouldn't be complicated but there is something wrong with the mate scores, or even something even more serious... I am very grateful for any input, I have spent way too long not figuring this out :(
User avatar
lithander
Posts: 915
Joined: Sun Dec 27, 2020 2:40 am
Location: Bremen, Germany
Full name: Thomas Jahn

Re: Transposition tables are hard...

Post by lithander »

It's great to hear that you found Leorik to be of some value in your development! The TT code that you have copied is a port from MinimalChess to Leorik but in MinimalChess I didn't use Negamax and so maybe there's a bug in there that I missed so far. That was my fear when I saw your post and so I wanted to follow your testcase in Leorik and see if it's having the same kind of problem.

Code: Select all

position fen k7/4P3/3B4/8/1p2p3/pP2P1P1/P1P1B1P1/4K3 w - - 0 1
go
info depth 1 score cp 1877 nodes 42 nps 8400 time 5 pv e7e8Q
info depth 2 score cp 1877 nodes 130 nps 16250 time 8 pv e7e8Q a8a7
info depth 3 score cp 1959 nodes 1124 nps 140500 time 8 pv e7e8Q a8b7 e8e4
info depth 4 score cp 2001 nodes 2972 nps 297200 time 10 pv e7e8Q a8b7 d6b4 b7c7
info depth 5 score cp 2010 nodes 19850 nps 1526923 time 13 pv e7e8Q a8b7 d6b4 b7b6 e8e6
info depth 6 score cp 2089 nodes 48197 nps 1338805 time 36 pv e7e8Q a8b7 d6b4 b7b6 b4a3 b6c7
info depth 7 score mate 4 nodes 290936 nps 3505253 time 83 pv e7e8Q a8b7 e8e4 b7b6 e4c4 b6a7 c4a6
info depth 8 score mate 4 nodes 634364 nps 1934036 time 328 pv e7e8Q a8b7 e8e4 b7b6 e4c4 b6a7 c4a6
info depth 9 score mate 4 nodes 3260834 nps 5276430 time 618 pv e7e8Q a8b7 e8e4 b7b6 e4c4 b6a7 c4a6
info depth 10 score mate 4 nodes 6324025 nps 3098493 time 2041 pv e7e8Q a8b7 e8e4 b7b6 e4c4 b6a7 c4a6
This looks good to me. So next you said:
Then if I play Ka7 it suddenly forgets the mate and print a completely nonsense PV line for the depths:
You can't play Ka7 in that position. It's white to move! What is the exact move order you tried or rather what's the commands you did feed to your engine?

Some general advice:

...I would try and tackle your problem step by step. If your engine outputs an illegal PV than I would try to fix that first. You keep track of your PV outside the TT (it's possible to try and reconstruct it from the TT but I've seen code where you store it in a triangular table) and I would just run a few matches in cutechess-cli because that will verify the PV given by the engine and report errors.

For that you don't even need the TT!

I agree that Transposition tables are kinda hard... but when I developed it I did so step by step, not following a tutorial or looking at the endresult in other engine but each component step by step with careful testing so when problems started to appear I knew exactly that it must have been introduced in just a the tiny few changes I made last.

Buckets are optional. And keeping track of the best move is a seperate feature from tracking the score with bounds of a position. Both features can be tested separately. Maybe after a TT hit research that line anyway and verify that result? That tanks the performance but verifies the implementation.

What my TT assumes that there are no hash collisions. E.g. two positions with the same zobrist hash are different. If you have a bug in your zobrist implemenation that might not be the case. So implement (temporarily?) an equality operator on your position and do that extra check that the stored bestmove and score is really of the position you're queriying and it's not just a hash collision.

That's all I can come up with for now. Sorry that it was more about testing strategy then actually looking at your code and spotting a bug. I skimmed it and found nothing obvious... but if it were obvious you'd have it found already, so I'm not surprised! ;)
Minimal Chess (simple, open source, C#) - Youtube & Github
Leorik (competitive, in active development, C#) - Github & Lichess
eligolf
Posts: 114
Joined: Sat Nov 14, 2020 12:49 pm
Full name: Elias Nilsson

Re: Transposition tables are hard...

Post by eligolf »

Thank you for the advice Thomas! And I am glad it works find in your engine hehe. It is super hard to know what is buggy and not in chess engines.. However, I have tried to play without the TT hit code and all works as expected then which makes me think it is just the TT screwing things up.

As for the move order I mean that after the engine plays Bc4 which is correct, I respond with Ka7, and then from that position the engine gets the next output that I showed. All played out in Arena.

As you say, maybe I would try to get the PV from the TT instead, it is just I have never done it before so I will have to figure out how :) I don't think there is a problem with zobrist hashing, I have tested it pretty thouroughly, as far as possible at least. And this problem arises no matter mating position so I think it has something to do with mate sores and how they are stored.

The code in Leorik is super helpful at times, but I have no clue how your search works :P I only know the standard minimax/negamax framework and how things work there.
User avatar
lithander
Posts: 915
Joined: Sun Dec 27, 2020 2:40 am
Location: Bremen, Germany
Full name: Thomas Jahn

Re: Transposition tables are hard...

Post by lithander »

eligolf wrote: Fri Feb 25, 2022 11:56 am The code in Leorik is super helpful at times, but I have no clue how your search works :P I only know the standard minimax/negamax framework and how things work there.
It's based on negamax, too! ;) Just with staged move generation and PVS.

And Leorik doesn't have a problem with the position and moves you gave me. So using my TT temporarily should still be safe.
Minimal Chess (simple, open source, C#) - Youtube & Github
Leorik (competitive, in active development, C#) - Github & Lichess
pedrojdm2021
Posts: 157
Joined: Fri Apr 30, 2021 7:19 am
Full name: Pedro Duran

Re: Transposition tables are hard...

Post by pedrojdm2021 »

maybe you're doing somerthing wrong somewhere...

can you show your code for lookup / storing?

also, if you're following the code monkey king yt series, he has another specific video about transposition tables that you need to apply to it.

this is a must to apply when you're using transposition tables:


if you forgot doing that stuff, then implement it and try again, good luck
Chessnut1071
Posts: 313
Joined: Tue Aug 03, 2021 2:41 pm
Full name: Bill Beame

Re: Transposition tables are hard...

Post by Chessnut1071 »

eligolf wrote: Fri Feb 25, 2022 10:29 am Hi,

Transposition tables are very frustrating, I think I understand what is going on but then engine does something crazy :)
I first implemented it according to Bruce's website and following along Maksims Youtube tutorials. Then I changed to an approach similar to Leorik and Cosette for the store/read but kept the search store/read places as before.

All seems fine and it plays as before implementing the tables, but with a reduced node count of around half. All good I thought.. Then I got to the endgame and saw some really strange behaviors, when it was finding/not finding mates which makes me think something is wrong with how I store these scores. Since I followed all tutorials and have checked the code for a around a week without finding what can be wrong I am now hoping someone here is able to point me in the right direction.

Given this position it finds the mate in 4 with Bc4 at depth 8 and onwards.
[fen]k7/4P3/3B4/8/1p2p3/pP2P1P1/P1P1B1P1/4K3 w - - 0 1[/fen]


The moves in PV looks a bit strange though and it looks like it thinks white plays twice in a row:

Code: Select all

 1	00:00	 24	8	+13,69	e7-e8Q+
 2	00:00	 103	8	+14,13	e7-e8Q+ Ka8-b7
 3	00:00	 318	8	+14,13	e7-e8Q+ Ka8-b7 Bd6xb4
 4	00:00	 560	560k	+18,53	Bd6xb4 Ka8-b8 e7-e8Q+ Kb8-b7
 5	00:00	 3k	481k	+15,83	Be2-a6 Ka8-a7 Bd6xb4 Ka7xa6 e7-e8Q
 6	00:00	 9k	429k	+18,63	Be2-c4 Ka8-a7 e7-e8Q Ka7-b7 Bd6xb4 Kb7-a7
 7	00:00	 56k	607k	+18,90	Bd6xb4 Ka8-b8 e7-e8Q+ Kb8-a7 Bb4xa3 Ka7-b7
 8	00:00	 148k	449k	+M4	Be2-c4 Ka8-b7 e7-e8Q Kb7-b6 Kb6-c6
 9	00:02	 1 441k	643k	+M4	Be2-c4 Ka8-b7 e7-e8Q Kb7-b6 Kb6-a5
 10	00:03	 1 823k	574k	+M4	Be2-c4 Ka8-a7 e7-e8Q Ka7-b7 Kb7-c6
 11	00:07	 4 631k	629k	+M4	Be2-c4 Ka8-a7 e7-e8Q Ka7-b7 Kb7-c6

Then if I play Ka7 it suddenly forgets the mate and print a completely nonsense PV line for the depths:

Code: Select all

 1	00:00	 24	8	+13,59	e7-e8Q
 2	00:00	 113	8	+18,05	e7-e8Q Ka7-b7
 3	00:00	 379	190k	+18,05	e7-e8Q Ka7-b7 Bd6xb4
 4	00:00	 1k	274k	+18,59	Bc4-d5 Ka7-a6 e7-e8Q Ka6-a7
 5	00:00	 4k	366k	+18,63	e7-e8Q Ka7-b7 Bd6xb4 Kb7-a7 Bb4xa3
 6	00:00	 12k	349k	+16,13	Bc4-a6 Ka7xa6 e7-e8Q Ka6-a5 Ka5-b5
 7	00:00	 37k	410k	+M3	e7-e8Q Ka7-b7 Kb7-c6
 8	00:00	 60k	409k	+19,33	Bd6xb4 Ka7-b7 Bb4xa3 Kb7-b8 e7-e8Q+ Kb8-a7 Bc4-f7 Ka7-b7
 9	00:00	 163k	488k	+19,50	Bd6xb4 Ka7-a8 e7-e8Q+ Ka8-b7 Bb4xa3 Kb7-b6 Kb6-a7 e4xe8
 10	00:00	 316k	435k	+20,01	Bd6xb4 Ka7-b8 Bb4xa3 Kb8-a8 e7-e8Q+ Ka8-a7 Bc4-d5 Ka7-b6 Ba3-d6 Kb6-a7
 11	00:01	 688k	494k	+M5	Bd6xb4 Ka7-a8 Bb4-d6 Ka8-a7 e7-e8Q Ka7-b7 Kb7-a8 Bc4-d5+
I read the TT at the beginning after finding if it is draw. Then I store it in 3 places; when depth is 0 with flag Exact, when beta cutoff and at the end before returning, see code below.

NEGAMAX code (with some storing and other things cleaned for simplicity of reading:

Code: Select all

private int Negamax(int depth, int ply, int alpha, int beta)
        {
            // Find if it is a draw
            if (boardState.CheckForDraw()) return 0;

            // Init variables
            int score;
            bool foundPV = false;
            bool isPV = (beta - alpha) > 1;

            // Check transpositions (not in PV nodes, not when ply == 0 and not when we don't have an entry)
            if (Transpositions.ReadTT(boardState.ZobristKey, depth, ply, alpha, beta, out Move _bestMove, out int _score))
            {
                if (!isPV && ply != 0)
                {
                    return _score;
                }
            }

            // Check if we reached depth 0 and go into Quiscence search
            if (depth == 0)
            {
                score = QuiescenceSearch(ply, alpha, beta);
                Transpositions.WriteTT(boardState.ZobristKey, depth, ply, alpha, beta, score, bestMove);
                return score;
            }
            
            // Init PV capturing tables
            pvTable[ply][ply] = new Move();
            pvLength[ply] = 0;

            // Check if we are in check
            bool isInCheck = boardState.IsSquareAttacked(boardState.ColorToMove, ownKingIndex);            

            // Get legal moves
            Move[] children = boardState.GetAllMoves();
            SortMoves(ply, children);
            
            // Init number of legal moves and moves searched
            int legalMoves = 0;

            // Negamax loop
            foreach (Move move in children)
            {
                // Break if finding illegal move
                if (move == new Move()) break;

                // Try if move is legal
                if (boardState.MakeMove(move))
                {
                    // Increase legal moves and node count
                    legalMoves++;

                    // Search and unmake move
                    score = -Negamax(depth - 1, ply + 1, -beta, -alpha);
                    boardState.UnmakeMove(move);

                    // Found a better move
                    if (score > alpha)
                    {
                        foundPV = true;
                        alpha = score;

                        // Write PV move to PV table for the given ply
                        pvTable[ply][ply] = move;

                        // Loop over the next ply
                        for (int j = 0; j < pvLength[ply + 1]; j++)
                        {
                            // Copy move from deeper ply into current ply's line
                            pvTable[ply][ply + 1 + j] = pvTable[ply + 1][ply + 1 + j];
                        }

                        // Adjust PV lenght
                        pvLength[ply] = 1 + pvLength[ply + 1];

                        // Fail hard beta cutoff (node fails high)
                        if (score >= beta)
                        {
                            // Store transposition tabel entry
                            Transpositions.WriteTT(boardState.ZobristKey, depth, ply, alpha, beta, score, bestMove); ;

                            return beta;
                        }
                    }
                }                
            }

            // If we don't have any legal moves in the position, check if it is mate or stalemate
            if (legalMoves == 0)
            {
                // If checkmate, return the mate value, else stalemate value
                if (isInCheck) return -SearchConstants.mateValue + ply;
                else return 0;
            }

            // Write to transposition table wih score = alpha
            Transpositions.WriteTT(boardState.ZobristKey, depth, ply, alpha, beta, alpha, bestMove);

            // Node fails low
            return alpha;
        }
Here is code for read and write to TT, right now it is taken from Leorik straight off, I will change this when I understand what is wrong with the implementation I have :)

Code: Select all

public static bool ReadTT(ulong zobristHash, int depth, int ply, int alpha, int beta, out Move bestMove, out int score)
        {
            bestMove = default;
            score = 0;
            if (!Index(zobristHash, out int index))
                return false;

            ref HashEntry entry = ref hashTable[index];
            bestMove = entry.BestMove;

            if (entry.Depth < depth)
                return false;

            score = AdjustMateDistance(entry.Score, -ply);

            //1.) score is exact and within window
            if (entry.Flag == (byte)Flag.Exact)
                return true;
            //2.) score is below floor
            if (entry.Flag == (byte)Flag.Alpha && score <= alpha)
                return true; //failLow
            //3.) score is above ceiling
            if (entry.Flag == (byte)Flag.Beta && score >= beta)
                return true; //failHigh

            return false;           
        }       

Code: Select all

public static void WriteTT(ulong zobristHash, int depth, int ply, int alpha, int beta, int score, Move bestMove)
        {
            ref HashEntry entry = ref hashTable[Index(zobristHash)];

            // Don't overwrite a bestmove with 'default' unless it's a new position
            if (entry.Key != zobristHash || bestMove != default)
                entry.BestMove = bestMove;

            entry.Key = zobristHash;
            entry.Depth = depth < 0 ? default : (byte)depth;
            entry.Age = 0;

            if (score >= beta)
            {
                entry.Flag = (byte)Flag.Beta;
                entry.Score = AdjustMateDistance(beta, ply);
            }
            else if (score <= alpha)
            {
                entry.Flag = (byte)Flag.Alpha;
                entry.Score = AdjustMateDistance(alpha, ply);
            }
            else
            {
                entry.Flag = (byte)Flag.Exact;
                entry.Score = AdjustMateDistance(score, ply);
            }
        }

Code: Select all

        public static short AdjustMateDistance(int score, int ply)
        {
            if (Math.Abs(score) > SearchConstants.mateScore)
                return (short)(score + Math.Sign(score) * ply);
            else
                return (short)score;
        }
Sorry for all the code, I hope I am missing something obvious in where I read or write to the TT and the input values I use there. It shouldn't be complicated but there is something wrong with the mate scores, or even something even more serious... I am very grateful for any input, I have spent way too long not figuring this out :(
One of the problems may be that there are multiple mates in that position. Be5 & Bf4 are also mates in addition to Bc4. Try using a known single mate to avoid multiple solutions.
Tearth
Posts: 70
Joined: Thu Feb 25, 2021 5:12 pm
Location: Poland
Full name: Pawel Osikowski

Re: Transposition tables are hard...

Post by Tearth »

I always end the search at the end of iteration if the mate is detected AND the current depth is equal or larger than the mate's one - this prevents one of these strange situations when the engine is not able to end the game because the transposition table repeatedly changes the line without actually finishing by mate, or going into some wild ones. Not sure if it's "canonical", but so far worked pretty well in my engines.

PS. I had these issues especially when I implemented sharing transposition table between distinct searches, but maybe it will be somehow helpful here too.
eligolf
Posts: 114
Joined: Sat Nov 14, 2020 12:49 pm
Full name: Elias Nilsson

Re: Transposition tables are hard...

Post by eligolf »

Hmm I put the TT lookup after the check for depth == 0, as done in the CPW engine and now it suddenly works fine. I wonder if this could be the issue all along or if it was just luck that it now works in the given position and also in other positions I have tried so far.
User avatar
hgm
Posts: 28353
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Transposition tables are hard...

Post by hgm »

Staring at the code rarely solves anything, even when you do it for hours / days. Just take a look at what the engine is doing that leads to a wrong result through judiciously placed print statements, and yu will find out what the problem is within a few minutes.
User avatar
lithander
Posts: 915
Joined: Sun Dec 27, 2020 2:40 am
Location: Bremen, Germany
Full name: Thomas Jahn

Re: Transposition tables are hard...

Post by lithander »

eligolf wrote: Sat Feb 26, 2022 11:52 am Hmm I put the TT lookup after the check for depth == 0, as done in the CPW engine and now it suddenly works fine. I wonder if this could be the issue all along or if it was just luck that it now works in the given position and also in other positions I have tried so far.
So you were storing and reading depth-0 positions (which is essentially just qsearch) in the TT? And now you go directly into Qsearch and return that result?

While I'm not aware of any reason why the TT shouldn't work when the depth parameter is 0 what you do now is how the TT is used in Leorik. I never stored results from the qsearch, either.

And looking at the code you posted above I do wonder where is the bestMove variable is set so that when you call WriteTT and store it it's really actually the best move? is it a global variable?
Minimal Chess (simple, open source, C#) - Youtube & Github
Leorik (competitive, in active development, C#) - Github & Lichess