Transposition table causes illegal moves

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

niel5946
Posts: 174
Joined: Thu Nov 26, 2020 10:06 am
Full name: Niels Abildskov

Transposition table causes illegal moves

Post by niel5946 »

Hi.

I recently made a post concerning illegal moves as a result of faulty updating of the PV-stack (http://talkchess.com/forum3/viewtopic.php?f=7&t=76708 , the code referenced in the links are not the corrected version since I haven't pushed the changes yet), but after correcting this, I have isolated yet another cause of illegal moves: The transposition table.
The way I probe and order the hash moves are:
In root search:

Code: Select all

		bool ttHit = false;
		TT_Entry* entry = tt->probe_tt(ss->pos->posKey, ttHit);
		unsigned int pvMove = NOMOVE;
		
		if (ttHit) {
			pvMove = entry->data.move;
		
			// Loop through the move list and find the pvMove
			for (int m = 0; m < moves.size(); m++) {
				if (moves[m]->move == pvMove) {
					moves[m]->score = hash_move_sort;
					break;
				}
			}
		}
And in the main search function:

Code: Select all

		// Step 1. Transposition table probing (~30 elo - too little?). This is done before quiescence since it is quite fast, and if we can get a cutoff before
		// going into quiescence, we'll of course use that. Probing before quiescence search contributed with ~17 elo.
		bool ttHit = false;
		TT_Entry* entry = tt->probe_tt(ss->pos->posKey, ttHit);
		
		int ttScore = (ttHit) ? value_from_tt(entry->data.score, ss->pos->ply) : -INF;
		unsigned int ttMove = (ttHit) ? entry->data.move : NOMOVE;
		int ttDepth = (ttHit) ? entry->data.depth : 0;
		int ttFlag = (ttHit) ? entry->data.flag : NO_FLAG;
		
		if (ttHit && ttDepth >= depth) {
			switch (ttFlag) {
			case ALPHA:
				if (ttScore <= alpha) {
					pvLine->length = 0;
					return alpha;
				}
				break;
			case BETA:
				if (ttScore >= beta) {
					pvLine->length = 0;
					return beta;
				}
				break;
			case EXACT:
				pvLine->length = 0;
				return ttScore;
			}
		}
		
		/*
		...
		Pruning methods
		...
		*/
		
		// Just before looping through the moves list:
		// If the transposition table returned a move, this is probably the best, so we'll score it highest.
		if (ttHit && ttMove != NOMOVE) {
			for (int i = 0; i < moves.size(); i++) {
				if (moves[i]->move == ttMove) {
					moves[i]->score = hash_move_sort;
				}
				break;
			}
		}
Storing to and probing the TT:

Code: Select all

TT_Entry* TranspositionTable::probe_tt(uint64_t key, bool& hit) {
	TT_Entry* slot = &entries[key % numEntries];
	uint64_t* data = (uint64_t*) &slot->data;

	if (slot->key == (key ^ *data)) {
		hit = true;
		return slot;
	}

	hit = false;
	return nullptr;
}


void TranspositionTable::store_entry(const GameState_t* pos, int move, int score, int depth, int flag) {
	TT_Entry* slot = &entries[pos->posKey % numEntries];
	uint64_t* data = (uint64_t*) &slot->data;

	assert(flag >= 0 && flag <= 2);

	if (depth >= slot->data.depth || depth == slot->data.depth - 1) {
		slot->key = pos->posKey ^ *data;
		slot->data.move = move;
		slot->data.score = value_to_tt(score, pos->ply);
		slot->data.depth = depth;
		slot->data.flag = flag;
	}
}
Where a TT_Entry and data is:

Code: Select all

struct EntryData {
	volatile unsigned int move = NOMOVE;
	volatile int score = -INF;
	volatile unsigned int depth = 0;
	volatile int flag = NO_FLAG;
};


struct TT_Entry {
	volatile uint64_t key = 0;

	EntryData data;
};
As you can see, I don't use the hash move directly but instead I just give it a high score for ordering which means that if it were to be illegal, no moves would be scored. I have checked many positions where it gave illegal moves with perft just to make sure that the move generation isn't the issue.

For good measure, the entire search_root function is:

Code: Select all

	// Root alpha beta
	int search_root(SearchThread_t* ss, int depth, int alpha, int beta, SearchPv* pvLine) {
		assert(depth > 0);

		ss->info->nodes++;

		pvLine->clear();
		SearchPv line;
		
		int score = -INF;
		int best_score = -INF;
		int best_move = NOMOVE;
		int new_depth = depth;

		bool raised_alpha = false;
		int legal = 0;

		MoveList moves; 
		ss->generate_moves(&moves);


		// Step 1. In-check extensions.
		bool in_check = ss->pos->in_check();
		//if (in_check) {
		//	new_depth++;
		//}

		
		// Steo 2. Static evaluation.
		if (in_check) {
			ss->static_eval[ss->pos->ply] = VALUE_NONE;
		}
		ss->static_eval[ss->pos->ply] = Eval::evaluate(ss->pos);


		// Step 3. Probe transposition table --> If there is a move from previous iterations, we'll assume the best move from that as the best move now, and
		//	order that first.
		bool ttHit = false;
		TT_Entry* entry = tt->probe_tt(ss->pos->posKey, ttHit);
		unsigned int pvMove = NOMOVE;
		
		if (ttHit) {
			pvMove = entry->data.move;
		
			// Loop through the move list and find the pvMove
			for (int m = 0; m < moves.size(); m++) {
				if (moves[m]->move == pvMove) {
					moves[m]->score = hash_move_sort;
					break;
				}
			}
		}


		// Now we'll loop through the move list.
		for (int m = 0; m < moves.size(); m++) {
			line.clear();

			ss->pickNextMove(m, &moves);
			
			unsigned int move = moves[m]->move;
			

			if (!ss->pos->make_move(moves[m])) {
				continue;
			}
			
			legal++;

			
			// Print move, move number and such to the command line.
			if (ss->thread_id == 0) {
				uci_moveinfo(move, depth, legal);
			}

			
			// Set the previous move such that we can use the countermove heuristic.
			ss->moves_path[ss->pos->ply] = move;

			// Step 3. Principal Variation search. We search all moves with the full window until one raises alpha. Afterwards we'll search with a null window
			//		and only widen it if the null window search raises alpha, which is assumed unlikely.
			if (!raised_alpha) {
				score = -alphabeta(ss, new_depth - 1, -beta, -alpha, true, &line);
			}
			else {
				score = -alphabeta(ss, new_depth - 1, -alpha - 1, -alpha, true, &line);

				if (score > alpha && score < beta) {
					score = -alphabeta(ss, new_depth - 1, -beta, -alpha, true, &line);
				}
			}


			ss->pos->undo_move();

			if (ss->info->stopped) { return 0; }


			if (score >= beta) { // Fail high
				if (legal == 1) {
					ss->info->fhf++;
				}
				ss->info->fh++;

				tt->store_entry(ss->pos, move, score, depth, BETA);


				return beta;
			}

			if (score > best_score){
				best_score = score;

				if (score > alpha) {
					alpha = score;
					best_move = move;

					raised_alpha = true;

					ChangePV(best_move, pvLine, &line);
				}
			}
		}

		if (legal == 0) {
			if (ss->pos->in_check()) {
				return -INF + ss->pos->ply;
			}
			else {
				return 0;
			}
		}
		
		// If we improved alpha, we're in a PV-node
		if (raised_alpha) {
			assert(best_move == pvLine->pv[0]);

			tt->store_entry(ss->pos, best_move, alpha, depth, EXACT);
		}
		else {
			tt->store_entry(ss->pos, best_move, alpha, depth, ALPHA);
		}
	
		return alpha;
	}
I have played around 600 games with both a version with TT disabled and one with it being enabled, and only the latter gave illegal moves (1% of the games to be exact).

I really don't understand how the TT can cause illegal moves when I don't even use the hash move (if found) directly, but only if it already is in the list of pseudo-legal moves.

Is this a bug in the code or just a result of some sort of collisions in the TT? If the former: Could this be caused by the above code snippets?

Thanks for the help in advance! :)
Author of Loki, a C++ work in progress.
Code | Releases | Progress Log |