Perft using nullmove

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

User avatar
Lasse Hansen
Posts: 27
Joined: Wed May 28, 2008 1:07 pm
Location: Porsgrunn, Norway

Perft using nullmove

Post by Lasse Hansen »

Some time ago I thought I should try out some of the features of C++, and eventually rewrite my engine Sillycon from scratch using C++.
Then of course the first step is to write a move generator, and this needs a perft function for verification.
Since I have seen some other using nullmove in perft calculations, I also gave this a try.
My results were faster than I expected. Here are some results compared to:
1. Mine with nullmove and popcount
2. Mine with popcount
3. Mine with generating all moves to movelist, then bulk count
4. gperft 1.1 by Paul Byrne (using nullmove as I understand)
5. qperft.exe by H.G.Müller
Free. This tells the fraction of nodes found by nullmove in column 1.

All results are single threaded, no unique positions, no hash, on an i7 2600K@3.4GHz

CPW 1: Start position
FEN rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1

Code: Select all

                  1      2       3      4        5        Free
d6   119060324	 0.13	0.25    0.44            0.700    71.20%
d7  3195901860	 3.47	6.38   11.42   5.000   18.000    66.43%
d8 84998978956	92.96 172.66  311.96 135.000  480.000    64.55%
CPW 2: Kiwipete position
FEN r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w QKqk - 0 1

Code: Select all

                   1      2      3      4       5      Free
d5    193690690   0.20   0.28   0.60    -      0.987  40.39%
d6   8031647685   9.61  12.55  26.71  12.046  44.081  29.85%
d7 374190009323 385.60 540.42    -   514.651    -     39.16%
Sorry for the long post, but I didnt want to try to explain this by words.
Of course, if there are any questions, Ill be happy to answer.

For doing nullmove, I generate some helper bitboards:

Code: Select all

	// Perft nullmove helper variables
	U64 BtmSpace;		// BTM move space = most attacks + piece occupation + potential pawn pushes
	U64 NoTouch;		// Squares around BTM king that must not be attacked
	U64 DoTouch;		// Squares around BTM king that must be attacked
	U64 NoIntercept;	// Squares in slider rays that block attacks on DoTouch
	U64 Pinned2NoTouch;	// WTM pieces pinned to NoTouch by WTM sliders
	U64 WtmAtks;		// WTM attacks union
	U64 ExtBishKVA;		// BTM king xray vulnerability, for where QB (QR) can not move freely
	U64 ExtRookKVA;		// into, because it will make pins (when not already pinned)
	U64 NoTouchBishKVA; // Not to set QB on (prevents BTM king moves to NoTouch bb)
	U64 NoTouchRookKVA; // Not to set QR on
Some defines used:

Code: Select all

#define QR(c) (Pcs[BQueen+8*c] | Pcs[BRook+8*c])
#define QB(c) (Pcs[BQueen+8*c] | Pcs[BBishop+8*c])
#define QMASK(c) (Mag[st->KingSquare[c]].RMask | Mag[st->KingSquare[c]].BMask)
#define QKVA (st->BishKVA | st->RookKVA)
BtmSpace is generated by the GenSpace function:

Code: Select all

template <TColour WTM> 
U64 PositionClass&#58;&#58;GenSpace&#40;PosState *st&#41;
// Generates a BB of squares not to be moved from or to when using nullmove in perft.
&#123;
	U64 Atk = Pcs&#91;BPcs+WTM&#93;;	// Piece occupation
	U64 Pieces = Pcs&#91;BRook+8*WTM&#93; | Pcs&#91;BQueen+8*WTM&#93;;
	while &#40;Pieces&#41; &#123;
		int fr = FirstOne&#40;Pieces&#41;; Pieces &= Pieces-1;
		U64 Ptn = RookAttacks&#40;fr, Pcs&#91;AllPcs&#93;);
		Atk |= SetBit&#40;fr&#41; & BORDER ? Ptn &#58; Ptn & ~BORDER;
	&#125;  
	Pieces = Pcs&#91;BBishop+8*WTM&#93; | Pcs&#91;BQueen+8*WTM&#93;;
	while &#40;Pieces&#41; &#123; 
		int fr = FirstOne&#40;Pieces&#41;; Pieces &= Pieces-1;
		Atk |= BishAttacks&#40;fr, Pcs&#91;AllPcs&#93;) & ~BORDER;
	&#125;  
	Pieces = Pcs&#91;BPawn+8*WTM&#93;;
	Atk |= WTM ? (~FILE_A & Pieces&#41;>>7 &#58; (~FILE_A & Pieces&#41;<<9;
	Atk |= WTM ? (~FILE_H & Pieces&#41;>>9 &#58; (~FILE_H & Pieces&#41;<<7;
	Atk |= WTM ? Pieces>>8 &#58; Pieces<<8; 	// Pawn push

	Pieces = WTM ? &#40;Pieces & RANK_2&#41;>>8 &#58; &#40;Pieces & RANK_7&#41;<<8; 
	Pieces &= ~Pcs&#91;AllPcs&#93;;
	Atk |= WTM ? Pieces>>8 &#58; Pieces<<8;		// Pawn double push
	
	Atk |= KingAttacks&#91;st->KingSquare&#91;WTM&#93;&#93;;
	return Atk;
&#125;
The other helper bitboards are generated by:

Code: Select all

template <TColour WTM> 
void PositionClass&#58;&#58;GenNullMoveBBs&#40;PosState *st&#41;
//	Generates BtmSpace, NoTouch, DoTouch, NoIntercept, Pinned2NoTouch, WtmAtks
&#123;
	const TColour BTM = WTM ? BLACK &#58; WHITE;	
	U64 Atk, Pieces, Ptn;
	int ksqr = st->KingSquare&#91;BTM&#93;;
	
	BtmSpace = GenSpace<BTM>&#40;st&#41;;
	NoTouch = ~Pcs&#91;BPcs+BTM&#93; & KingAttacks&#91;ksqr&#93; | SetBit&#40;ksqr&#41;;
	if &#40;st->CastlingRights&#41; &#123;
		if &#40;WTM && !&#40;Pcs&#91;AllPcs&#93; & 0x0000000000000006ull&#41; && st->CastlingRights & SetBit&#40;H8&#41;) NoTouch |= SetBit&#40;G8&#41;;
		if &#40;WTM && !&#40;Pcs&#91;AllPcs&#93; & 0x0000000000000070ull&#41; && st->CastlingRights & SetBit&#40;A8&#41;) NoTouch |= SetBit&#40;C8&#41;;
		if &#40;BTM && !&#40;Pcs&#91;AllPcs&#93; & 0x0600000000000000ull&#41; && st->CastlingRights & SetBit&#40;H1&#41;) NoTouch |= SetBit&#40;G1&#41;;
		if &#40;BTM && !&#40;Pcs&#91;AllPcs&#93; & 0x7000000000000000ull&#41; && st->CastlingRights & SetBit&#40;A1&#41;) NoTouch |= SetBit&#40;C1&#41;;
	&#125;
	NoIntercept = 0; Pinned2NoTouch = 0; WtmAtks = 0;
	Pieces = QR&#40;WTM&#41;;
	while &#40;Pieces&#41; &#123;	// WtmAtks, Pinned2NoTouch, NoIntercept
		int fr = FirstOne&#40;Pieces&#41;; Pieces &= Pieces-1;
		WtmAtks |= Atk = RookAttacks&#40;fr, Pcs&#91;AllPcs&#93;);
		if (&#40;Mag&#91;fr&#93;.RMask & NoTouch&#41; == 0&#41; continue; 
		// Direct attacks on NoTouch and BTM pieces in BTM king vicinity
		Ptn = Atk & &#40;NoTouch | Pcs&#91;BPcs+BTM&#93; & KingAttacks&#91;ksqr&#93;);
		while &#40;Ptn&#41; &#123; 
			NoIntercept |= Atk & RookAttacks&#40;FirstOne&#40;Ptn&#41;, Pcs&#91;AllPcs&#93;);
			Ptn &= Ptn-1;
		&#125;
		// Pinned2NoTouch = WTM pieces &#40;maybe&#41; pinned to NoTouch
		Ptn = Atk & Pcs&#91;BPcs+WTM&#93;; // Candidate pinned pieces
		Ptn = NoTouch & RookAttacks&#40;fr, Pcs&#91;AllPcs&#93; ^ Ptn&#41;; // Xray attacks on NoTouch
		if &#40;Ptn&#41; &#123;
//			Pinned2NoTouch |= Atk & Pcs&#91;BPcs+WTM&#93; & Mag&#91;FirstOne&#40;Ptn&#41;&#93;.RMask;
			Pinned2NoTouch |= Atk & Pcs&#91;BPcs+WTM&#93; & RookAttacks&#40;FirstOne&#40;Ptn&#41;, Pcs&#91;AllPcs&#93;);
			Ptn &= Ptn-1;
		&#125;
	&#125;
	Pieces = QB&#40;WTM&#41;;
	while &#40;Pieces&#41; &#123;	// WtmAtks, Pinned2NoTouch, NoIntercept
		int fr = FirstOne&#40;Pieces&#41;; Pieces &= Pieces-1;
		WtmAtks |= Atk = BishAttacks&#40;fr, Pcs&#91;AllPcs&#93;);
		if (&#40;Mag&#91;fr&#93;.BMask & NoTouch&#41; == 0&#41; continue; 
		// Direct attacks on NoTouch and BTM pieces in BTM king vicinity
		Ptn = Atk & &#40;NoTouch | Pcs&#91;BPcs+BTM&#93; & KingAttacks&#91;ksqr&#93;);
		while &#40;Ptn&#41; &#123; // Black bishop at e3 may attack g1 and c1
			NoIntercept |= Atk & BishAttacks&#40;FirstOne&#40;Ptn&#41;, Pcs&#91;AllPcs&#93;);
			Ptn &= Ptn-1;
		&#125;
		// Pinned2NoTouch = WTM pieces &#40;maybe&#41; pinned to NoTouch
		Ptn = Atk & Pcs&#91;BPcs+WTM&#93;; // Candidate pinned pieces
		Ptn = NoTouch & BishAttacks&#40;fr, Pcs&#91;AllPcs&#93; ^ Ptn&#41;; // Xray attacks on NoTouch
		if &#40;Ptn&#41; &#123;
//			Pinned2NoTouch |= Atk & Pcs&#91;BPcs+WTM&#93; & Mag&#91;FirstOne&#40;Ptn&#41;&#93;.BMask;
			Pinned2NoTouch |= Atk & Pcs&#91;BPcs+WTM&#93; & BishAttacks&#40;FirstOne&#40;Ptn&#41;,Pcs&#91;AllPcs&#93;);
			Ptn &= Ptn-1;
		&#125;
	&#125;
	WtmAtks |= KingAttacks&#91;st->KingSquare&#91;WTM&#93;&#93;;
	Pieces = Pcs&#91;BKnight+8*WTM&#93;;
	while &#40;Pieces&#41; &#123;
		WtmAtks |= KnightAttacks&#91;FirstOne&#40;Pieces&#41;&#93;;
		Pieces &= Pieces-1;
	&#125;
	WtmAtks |= WTM ? (~FILE_H & Pcs&#91;WPawn&#93;)>>9 &#58; (~FILE_H & Pcs&#91;BPawn&#93;)<<7; // East
	WtmAtks |= WTM ? (~FILE_A & Pcs&#91;WPawn&#93;)>>7 &#58; (~FILE_A & Pcs&#91;BPawn&#93;)<<9; // West

	DoTouch = NoTouch & WtmAtks;
	NoTouch ^= DoTouch; // If separated, a piece that is not attacking NoTouch may attack DoTouch 

	ExtBishKVA = ExtRookKVA = NoTouchBishKVA = NoTouchRookKVA = 0;
	if &#40;QB&#40;WTM&#41;) &#123; // Alternative code
		Ptn = &#40;BishAttacks&#40;ksqr, Pcs&#91;AllPcs&#93;) & Pcs&#91;BPcs+BTM&#93;) ^ Pcs&#91;AllPcs&#93;;
		ExtBishKVA = BishAttacks&#40;ksqr, Ptn&#41;; // Xray BTM pieces
		Ptn = NoTouch;
		while &#40;Ptn&#41; &#123;
			NoTouchBishKVA |= BishAttacks&#40;FirstOne&#40;Ptn&#41;, Pcs&#91;AllPcs&#93;);
			Ptn &= Ptn-1;
		&#125;
	&#125;
	if &#40;QR&#40;WTM&#41;) &#123; // Alternative code
		Ptn = &#40;RookAttacks&#40;ksqr, Pcs&#91;AllPcs&#93;) & Pcs&#91;BPcs+BTM&#93;) ^ Pcs&#91;AllPcs&#93;;
		ExtRookKVA = RookAttacks&#40;ksqr, Ptn&#41;; // Xray BTM pieces
		Ptn = NoTouch;
		while &#40;Ptn&#41; &#123;
			NoTouchRookKVA |= RookAttacks&#40;FirstOne&#40;Ptn&#41;, Pcs&#91;AllPcs&#93;);
			Ptn &= Ptn-1;
		&#125;
	&#125;
&#125;
and the perft function for nullmove is (only called when not in check):

Code: Select all

template <TColour WTM> 
U64 PositionClass&#58;&#58;PerftNullmove&#40;PosState *st&#41;
&#123;
	const TColour BTM = WTM ? BLACK &#58; WHITE;

	&#40;st+1&#41;->CastlingRights  = st->CastlingRights;  &#40;st+1&#41;->EpSquare = NO_SQUARE;
	&#40;st+1&#41;->KingSquare&#91;BTM&#93; = st->KingSquare&#91;BTM&#93;; &#40;st+1&#41;->KingSquare&#91;WTM&#93; = st->KingSquare&#91;WTM&#93;;
	CheckDetect<BTM>&#40;st+1&#41;; // Need KVA's for &#40;st+1&#41;
	int NullMoveCt = (&#40;st+1&#41;->RookKVA | &#40;st+1&#41;->BishKVA&#41; ? MovgenPopCount<BTM, ASSUME_PINS>&#40;st+1&#41;
														 &#58; MovgenPopCount<BTM, NO_PINS>&#40;st+1&#41;;
	GenNullMoveBBs<WTM>&#40;st&#41;;

	TMove MoveList&#91;MAXMOVES&#93;, *LM = MoveList, *M = MoveList;
	TMove MoveListNm&#91;MAXMOVES&#93;, *LastMoveNm, *Mnm = MoveListNm;
	int fr, to, FreeMoves = 0;
	int ksqr = st->KingSquare&#91;BTM&#93;;
	U64 Atk, RestAtk, Pieces, Ptn;
	//--------------------------------------------------------------------------
//	LM = MovgenQueens<WTM, ASSUME_PINS>&#40;st, LM&#41;;

	Pieces = Pcs&#91;BQueen+8*WTM&#93;;
	while &#40;Pieces&#41; &#123;
		fr = FirstOne&#40;Pieces&#41;; Pieces &= Pieces-1;
		if &#40;SetBit&#40;fr&#41; & &#40;QKVA | BtmSpace | ExtBishKVA | ExtRookKVA&#41;) &#123;
			LM = MovgenQueen<WTM, ASSUME_PINS>&#40;st, LM, fr&#41;;
			continue;
		&#125;
		Atk = RookAttacks&#40;fr, Pcs&#91;AllPcs&#93;) | BishAttacks&#40;fr, Pcs&#91;AllPcs&#93;);
		if &#40;Atk & DoTouch&#41; &#123;
			LM = MovgenQueen<WTM, NO_PINS>&#40;st, LM, fr&#41;;
			continue;
		&#125;
		Atk &= ~Pcs&#91;BPcs+WTM&#93;;
		RestAtk = Atk & &#40;BtmSpace | NoTouchBishKVA | NoTouchRookKVA | ExtBishKVA | ExtRookKVA&#41;;
		Atk ^= RestAtk;
		FreeMoves += PopCount&#40;Atk&#41;;
		fr = &#40;fr<<6&#41; + (&#40;BQueen+8*WTM&#41;<<12&#41;;
		while &#40;RestAtk&#41; &#123;
			to = FirstOne&#40;RestAtk&#41;; RestAtk &= RestAtk-1;
			*LM++ = fr + to + &#40;Board&#91;to&#93;<<16&#41;;
		&#125;
	&#125;
	//--------------------------------------------------------------------------
//	LM = MovgenRooks<WTM, ASSUME_PINS>&#40;st, LM&#41;;

	Pieces = Pcs&#91;BRook+8*WTM&#93;;
	while &#40;Pieces&#41; &#123;
		fr = FirstOne&#40;Pieces&#41;; Pieces &= Pieces-1;
		if &#40;SetBit&#40;fr&#41; & &#40;QKVA | BtmSpace | Pinned2NoTouch | ExtBishKVA | ExtRookKVA&#41;) &#123;
			LM = MovgenRook<WTM, ASSUME_PINS>&#40;st, LM, fr&#41;;
			continue;
		&#125;
		Atk = RookAttacks&#40;fr, Pcs&#91;AllPcs&#93;);
		if &#40;Atk & DoTouch&#41; &#123;
			LM = MovgenRook<WTM, NO_PINS>&#40;st, LM, fr&#41;;
			continue;
		&#125;
		Atk &= ~Pcs&#91;BPcs+WTM&#93;;
		RestAtk = Atk & &#40;BtmSpace | NoTouchBishKVA | NoTouchRookKVA | ExtBishKVA | ExtRookKVA&#41;;
		RestAtk |= Atk & NoIntercept;
		Atk ^= RestAtk;
		FreeMoves += PopCount&#40;Atk&#41;;
		fr = &#40;fr<<6&#41; + (&#40;BRook+8*WTM&#41;<<12&#41;;
		while &#40;RestAtk&#41; &#123;
			to = FirstOne&#40;RestAtk&#41;; RestAtk &= RestAtk-1;
			*LM++ = fr + to + &#40;Board&#91;to&#93;<<16&#41;;
		&#125;
	&#125;
	//--------------------------------------------------------------------------
//	LM = MovgenBishops<WTM, ASSUME_PINS>&#40;st, LM&#41;;

	Pieces = Pcs&#91;BBishop+8*WTM&#93;;
	while &#40;Pieces&#41; &#123;
		fr = FirstOne&#40;Pieces&#41;; Pieces &= Pieces-1;
		if &#40;SetBit&#40;fr&#41; & &#40;QKVA | BtmSpace | Pinned2NoTouch | ExtBishKVA | ExtRookKVA&#41;) &#123;
			LM = MovgenBishop<WTM, ASSUME_PINS>&#40;st, LM, fr&#41;;
			continue;
		&#125;
		Atk = BishAttacks&#40;fr, Pcs&#91;AllPcs&#93;);
		if &#40;Atk & DoTouch&#41; &#123;
			LM = MovgenBishop<WTM, NO_PINS>&#40;st, LM, fr&#41;;
			continue;
		&#125;
		Atk &= ~Pcs&#91;BPcs+WTM&#93;;
		RestAtk = Atk & &#40;BtmSpace | NoTouchBishKVA | NoTouchRookKVA | ExtBishKVA | ExtRookKVA&#41;;
		RestAtk |= Atk & NoIntercept;
		Atk ^= RestAtk;
		FreeMoves += PopCount&#40;Atk&#41;;
		fr = &#40;fr<<6&#41; + (&#40;BBishop+8*WTM&#41;<<12&#41;;
		while &#40;RestAtk&#41; &#123;
			to = FirstOne&#40;RestAtk&#41;; RestAtk &= RestAtk-1;
			*LM++ = fr + to + &#40;Board&#91;to&#93;<<16&#41;;
		&#125;
	&#125;
	//--------------------------------------------------------------------------
//	LM = MovgenKnights<WTM, ASSUME_PINS>&#40;st, LM&#41;;

	Pieces = Pcs&#91;BKnight+8*WTM&#93;;
	while &#40;Pieces&#41; &#123;
		fr = FirstOne&#40;Pieces&#41;; Pieces &= Pieces-1;
		if &#40;SetBit&#40;fr&#41; & &#40;QKVA | BtmSpace | Pinned2NoTouch | ExtBishKVA & ~BORDER | ExtRookKVA&#41;) &#123;
			LM = MovgenKnight<WTM, ASSUME_PINS>&#40;st, LM, fr&#41;;
			continue;
		&#125;
		Atk = KnightAttacks&#91;fr&#93;;
		if &#40;Atk & DoTouch&#41; &#123;
			LM = MovgenKnight<WTM, NO_PINS>&#40;st, LM, fr&#41;;
			continue;
		&#125;
		Atk &= ~Pcs&#91;BPcs+WTM&#93;;
		RestAtk = Atk & &#40;BtmSpace | ExtBishKVA & ~BORDER | ExtRookKVA&#41;;
		RestAtk |= Atk & NoIntercept;
		Ptn = RestAtk ^ Atk;
		while &#40;Ptn&#41; &#123;
			to = FirstOne&#40;Ptn&#41;; Ptn &= Ptn-1;
			if &#40;KnightAttacks&#91;to&#93; & NoTouch&#41; RestAtk |= SetBit&#40;to&#41;;
		&#125;		
		Atk ^= RestAtk;
		FreeMoves += PopCount&#40;Atk&#41;;
		fr = &#40;fr<<6&#41; + (&#40;BKnight+8*WTM&#41;<<12&#41;;
		while &#40;RestAtk&#41; &#123;
			to = FirstOne&#40;RestAtk&#41;; RestAtk &= RestAtk-1;
			*LM++ = fr + to + &#40;Board&#91;to&#93;<<16&#41;;
		&#125;
	&#125;
	//--------------------------------------------------------------------------
//	LM = MovgenPawns<WTM, ASSUME_PINS>&#40;st, LM&#41;;

	TMove PawnMoves&#91;32&#93;, *MPL;
	MPL = MovgenPawns<WTM, ASSUME_PINS>&#40;st, PawnMoves&#41;;
	M = PawnMoves;
	while &#40;M < MPL&#41; &#123;
		if &#40;SPECIAL2&#40;*M&#41;) &#123; *LM++ = *M++; continue; &#125; // en-passant and promotions 
		fr = FROM&#40;*M&#41;, to = TO&#40;*M&#41;;
		
		if &#40;SetBit&#40;fr&#41; & &#40;BtmSpace | QKVA | Pinned2NoTouch&#41;) &#123; *LM++ = *M++; continue; &#125;
		if &#40;SetBit&#40;fr&#41; & ~BORDER & &#40;ExtBishKVA | ExtRookKVA&#41;)  &#123; *LM++ = *M++; continue; &#125;

		if &#40;SetBit&#40;to&#41; & &#40;BtmSpace | NoIntercept&#41;) &#123; *LM++ = *M++; continue; &#125;
		if &#40;SetBit&#40;to&#41; & ~BORDER & &#40;ExtBishKVA | ExtRookKVA&#41;)  &#123; *LM++ = *M++; continue; &#125;
		
		if &#40;PawnAttacks&#91;WTM&#93;&#91;fr&#93; & DoTouch&#41; &#123; *LM++ = *M++; continue; &#125;
		if &#40;PawnAttacks&#91;WTM&#93;&#91;to&#93; & NoTouch&#41; &#123; *LM++ = *M++; continue; &#125;
		if &#40;BtmSpace & SetBit&#40;fr+8-16*WTM&#41;) &#123; *LM++ = *M++; continue; &#125; // Possible ep square
		FreeMoves++;
		M++;
	&#125;
	//--------------------------------------------------------------------------
//	LM = MovgenKing<WTM>&#40;st, LM&#41;;

	TMove KingMoves&#91;8&#93;, *MKL;
	MKL = MovgenKing<WTM>&#40;st, KingMoves&#41;;
	M = KingMoves;
	while &#40;M < MKL&#41; &#123;
		if &#40;SPECIAL2&#40;*M&#41;) &#123;*LM++ = *M++; continue; &#125; // castlings
		fr = FROM&#40;*M&#41;, to = TO&#40;*M&#41;;
		if &#40;SetBit&#40;fr&#41; & &#40;BtmSpace | Pinned2NoTouch | ExtBishKVA & ~BORDER | ExtRookKVA&#41;) &#123; *LM++ = *M++; continue; &#125;
		if &#40;SetBit&#40;to&#41; & &#40;BtmSpace | NoIntercept | ExtBishKVA & ~BORDER | ExtRookKVA&#41;) &#123; *LM++ = *M++; continue; &#125;
		if &#40;KingAttacks&#91;fr&#93; & DoTouch&#41; &#123; *LM++ = *M++; continue; &#125;
		if &#40;KingAttacks&#91;to&#93; & NoTouch&#41; &#123; *LM++ = *M++; continue; &#125;
		FreeMoves++;
		M++;
	&#125;
	//--------------------------------------------------------------------------
	Nfree += FreeMoves * NullMoveCt; // Global counter
	int RestNodes = 0; // Nodes found the hard way
	M = MoveList;
	while &#40;M < LM&#41; &#123;
		MakeMove<WTM,NO_HASH>&#40;st, *M&#41;;
		if &#40;CheckDetect<BTM>&#40;st+1&#41;) RestNodes += MovgenEvasionsPopCount<BTM>&#40;st+1&#41;;
		else &#123;
			if (&#40;st+1&#41;->BishKVA | &#40;st+1&#41;->RookKVA&#41; 
				 RestNodes += MovgenPopCount<BTM, ASSUME_PINS>&#40;st+1&#41;;
			else RestNodes += MovgenPopCount<BTM, NO_PINS>&#40;st+1&#41;;
		&#125;
		UnmakeMove<WTM>&#40;st, *M++);
	&#125;
	return &#40;U64&#41;&#40;FreeMoves * NullMoveCt + RestNodes&#41;;
&#125;
The king vulnerability bitboards BishKVA and RookKVA are generated by my CheckDetect function:

Code: Select all

template <TColour WTM>
int PositionClass&#58;&#58;CheckDetect&#40;PosState *st&#41;
// Detects if WTM is in check or double check
// Returns 0&#58; no check, 1&#58; single check, >1&#58; double check
// Generates BishKVA and RookKVA
// Generates CheckAtk, all squares to be intercepted for evading a single check
&#123;
	const TColour BTM = WTM ? BLACK &#58; WHITE;
	int ksqr = st->KingSquare&#91;WTM&#93;;

	st->RookKVA = st->BishKVA = 0;
	if &#40;Mag&#91;ksqr&#93;.RMask & QR&#40;BTM&#41;) &#123;
		st->RookKVA = RookAttacks&#40;ksqr, Pcs&#91;AllPcs&#93;);
		// See if st->RookKVA may be reduced
		if &#40;st->EpSquare == NO_SQUARE && &#40;st->RookKVA & QR&#40;BTM&#41;) == 0&#41; &#123; // Not in rook check
			U64 Atk = Pcs&#91;BPcs+WTM&#93; & st->RookKVA; // Target WTM pieces only
			Atk = RookAttacks&#40;ksqr, Pcs&#91;AllPcs&#93; ^ Atk&#41;; // Xray attacks
			if (&#40;Atk & QR&#40;BTM&#41;) == 0&#41; st->RookKVA = 0; // No rook pins
		&#125;
	&#125;
	if &#40;Mag&#91;ksqr&#93;.BMask & QB&#40;BTM&#41;) &#123;
		st->BishKVA = BishAttacks&#40;ksqr, Pcs&#91;AllPcs&#93;);
		if &#40;st->EpSquare == NO_SQUARE && &#40;st->BishKVA & QB&#40;BTM&#41;) == 0&#41; &#123; // Not in bishop check
			U64 Atk = Pcs&#91;BPcs+WTM&#93; & st->BishKVA; // Target WTM pieces only
			Atk = BishAttacks&#40;ksqr, Pcs&#91;AllPcs&#93; ^ Atk&#41;; // Xray attacks
			if (&#40;Atk & QB&#40;BTM&#41;) == 0&#41; st->BishKVA = 0;
		&#125;
	&#125;
	st->InCheck = 0;
	st->CheckAtk = 0ull;
	U64 Atk = PawnAttacks&#91;WTM&#93;&#91;ksqr&#93; & Pcs&#91;BPawn+8*BTM&#93;
			| KnightAttacks&#91;ksqr&#93; & Pcs&#91;BKnight+8*BTM&#93;;
	if &#40;Atk&#41; &#123;
		st->InCheck = 1;
		st->CheckAtk = Atk;
	&#125;
	Atk = st->RookKVA & QR&#40;BTM&#41;;
	if &#40;Atk&#41; &#123;
		st->InCheck += 1 + &#40;PopCount&#40;Atk&#41; > 1&#41;;
		Atk |= RookAttacks&#40;FirstOne&#40;Atk&#41;, Pcs&#91;AllPcs&#93;) & st->RookKVA;
		st->CheckAtk |= Atk; 
	&#125;
	Atk = st->BishKVA & QB&#40;BTM&#41;;
	if &#40;Atk&#41; &#123;
		st->InCheck += 1 + &#40;PopCount&#40;Atk&#41; > 1&#41;;
		Atk |= BishAttacks&#40;FirstOne&#40;Atk&#41;, Pcs&#91;AllPcs&#93;) & st->BishKVA;
		st->CheckAtk |= Atk; 
	&#125;
	return st->InCheck;
&#125;
User avatar
cdani
Posts: 2204
Joined: Sat Jan 18, 2014 10:24 am
Location: Andorra

Re: Perft using nullmove

Post by cdani »

Excuse my ignorance. What has to do perft with nullmove?
Thanks.
User avatar
Lasse Hansen
Posts: 27
Joined: Wed May 28, 2008 1:07 pm
Location: Porsgrunn, Norway

Re: Perft using nullmove

Post by Lasse Hansen »

cdani wrote:
Excuse my ignorance. What has to do perft with nullmove?
When remaining depth in perft is 1 (before end leaf) one may use nullmove to count the number of moves for the other side (BTM) = NullMoveCt. All moves= FreeMoves (for side to move WTM) that does not influence BTM moves, perft(1) may be counted as FreeMoves * NullMoveCt. This saves the effort of making the FreeMoves, doing perft(0), and unmaking FreeMoves. The perft function may look like this:

Code: Select all

template <TColour WTM> 
U64 PositionClass&#58;&#58;Perft&#40;PosState *st, int depth&#41;
&#123;
	const TColour BTM = WTM ? BLACK &#58; WHITE;
	TMove MoveList&#91;MAXMOVES&#93;, *LastMove, *M;

	CheckDetect<WTM>&#40;st&#41;;

	if &#40;depth == 0&#41; &#123;
		if &#40;st->InCheck&#41; return MovgenEvasions<WTM>&#40;st, MoveList&#41; - MoveList;
		if &#40;QKVA&#41; return MovgenPopCount<WTM, ASSUME_PINS>&#40;st&#41;;
		return MovgenPopCount<WTM, NO_PINS>&#40;st&#41;; 
	&#125;

	if (!st->InCheck && depth == 1&#41; return PerftNullmove<WTM>&#40;st&#41;; 

	U64 ct = 0;
	if &#40;st->InCheck&#41; LastMove = MovgenEvasions<WTM>&#40;st, MoveList&#41;;
	else &#123;
		if &#40;QKVA&#41; LastMove = Movgen<WTM, ASSUME_PINS>&#40;st, MoveList&#41;;
		else  LastMove = Movgen<WTM, NO_PINS>&#40;st, MoveList&#41;;
	&#125;
	M = MoveList;
	while &#40;M < LastMove&#41; &#123;
		MakeMove<WTM,NO_HASH>&#40;st, *M&#41;;
		ct += Perft<BTM>&#40;st+1, depth-1&#41;;
		UnmakeMove<WTM>&#40;st, *M++);
	&#125;
	return ct;
&#125;
ibid
Posts: 89
Joined: Mon Jun 13, 2011 12:09 pm

Re: Perft using nullmove

Post by ibid »

Lasse Hansen wrote:Some time ago I thought I should try out some of the features of C++, and eventually rewrite my engine Sillycon from scratch using C++.
Then of course the first step is to write a move generator, and this needs a perft function for verification.
Since I have seen some other using nullmove in perft calculations, I also gave this a try.
My results were faster than I expected. Here are some results compared to:
1. Mine with nullmove and popcount
2. Mine with popcount
3. Mine with generating all moves to movelist, then bulk count
4. gperft 1.1 by Paul Byrne (using nullmove as I understand)
5. qperft.exe by H.G.Müller
Free. This tells the fraction of nodes found by nullmove in column 1.

All results are single threaded, no unique positions, no hash, on an i7 2600K@3.4GHz

CPW 1: Start position
FEN rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1

Code: Select all

                  1      2       3      4        5        Free
d6   119060324	 0.13	0.25    0.44            0.700    71.20%
d7  3195901860	 3.47	6.38   11.42   5.000   18.000    66.43%
d8 84998978956	92.96 172.66  311.96 135.000  480.000    64.55%
CPW 2: Kiwipete position
FEN r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w QKqk - 0 1

Code: Select all

                   1      2      3      4       5      Free
d5    193690690   0.20   0.28   0.60    -      0.987  40.39%
d6   8031647685   9.61  12.55  26.71  12.046  44.081  29.85%
d7 374190009323 385.60 540.42    -   514.651    -     39.16%
Sorry for the long post, but I didnt want to try to explain this by words.
Of course, if there are any questions, Ill be happy to answer.
Hello,

gperft 1.1 uses a number of tricks, but not nullmove. The still unreleased 1.2 uses nullmove. I've put a windows binary for 1.2d here:
https://www.dropbox.com/s/9n0u2r048enjk ... d.zip?dl=0
This intermediate version should compute correct perfts, but has some bugs (certain command line options crash it). There are a number of optimization ideas I am still playing with before I release an official 1.2, but I am not expecting any significant speedup from this one. If you need a linux build (which is generally a little faster than windows), let me know, and I'll make one later tonight...

From the speedups between 1.1 and 1.2d on my machine, I would guess gperft might be a little faster than your nullmove version on the initial position and still noticably slower on kiwipete, but my machine is AMD so I'd love to see how it does on your machine.

I will take a look at your code later tonight when I have more time to see how similar our nullmove implementations are for perfts...

-paul
User avatar
Lasse Hansen
Posts: 27
Joined: Wed May 28, 2008 1:07 pm
Location: Porsgrunn, Norway

Re: Perft using nullmove

Post by Lasse Hansen »

A little update. If the code for pawn nullmove count is replaced by this:

Code: Select all

	TMove PawnMoves&#91;32&#93;, *MPL;
	MPL = MovgenPawns<WTM, ASSUME_PINS>&#40;st, PawnMoves&#41;;
	M = PawnMoves;
	while &#40;M < MPL&#41; &#123;
		if &#40;SPECIAL2&#40;*M&#41;) &#123; *LM++ = *M++; continue; &#125; // en-passant and promotions and captures
		fr = FROM&#40;*M&#41;, to = TO&#40;*M&#41;;
		
		if &#40;SetBit&#40;fr&#41; & &#40;BtmSpace | QKVA | Pinned2NoTouch&#41;) &#123; *LM++ = *M++; continue; &#125;
		if &#40;SetBit&#40;to&#41; & &#40;BtmSpace | NoIntercept&#41;) &#123; *LM++ = *M++; continue; &#125;

//		if &#40;SetBit&#40;fr&#41; & ~BORDER & &#40;ExtBishKVA | ExtRookKVA&#41;)  &#123; *LM++ = *M++; continue; &#125;
//		if &#40;SetBit&#40;to&#41; & ~BORDER & &#40;ExtBishKVA | ExtRookKVA&#41;)  &#123; *LM++ = *M++; continue; &#125;
		if &#40;Mag&#91;ksqr&#93;.BMask & QB&#40;WTM&#41;) &#123;
			if (&#40;SetBit&#40;fr&#41; | SetBit&#40;to&#41;) & ~BORDER & ExtBishKVA&#41; &#123; *LM++ = *M++; continue; &#125;
		&#125;
		if &#40;Mag&#91;ksqr&#93;.RMask & QR&#40;WTM&#41;) &#123; // Pawn MUST capture to get out of BORDER, pawn cant be on 1,8'th rank
			if (&#40;SetBit&#40;fr&#41; | SetBit&#40;to&#41;) & ~BORDER & ExtRookKVA&#41; &#123; *LM++ = *M++; continue; &#125;
		&#125;
		
		if &#40;PawnAttacks&#91;WTM&#93;&#91;fr&#93; & DoTouch&#41; &#123; *LM++ = *M++; continue; &#125;
		if &#40;PawnAttacks&#91;WTM&#93;&#91;to&#93; & NoTouch&#41; &#123; *LM++ = *M++; continue; &#125;
		if &#40;BtmSpace & SetBit&#40;fr+8-16*WTM&#41;) &#123; *LM++ = *M++; continue; &#125; // Possible ep square
		FreeMoves++;
		M++;
	&#125;
the speed of perft with nullmove now is(single thread, no hash, no unique):

Code: Select all

CPW 1&#58; Starting position
Perft 1             20    0.000s    0.00Mnps   0.00%              0
Perft 2            400    0.000s   36.36Mnps 100.00%            400
Perft 3           8902    0.000s  387.04Mnps  92.96%           8275
Perft 4         197281    0.000s  495.68Mnps  87.40%         172428
Perft 5        4865609    0.007s  670.47Mnps  80.67%        3925046
Perft 6      119060324    0.115s 1034.20Mnps  76.54%       91132336
Perft 7     3195901860    3.168s 1008.95Mnps  70.97%     2268291228
Perft 8    84998978956   87.564s  970.71Mnps  68.19%    57958532076
Perft 9  2439530234167 2594.017s  940.45Mnps  63.74%  1554878537163

CPW 2&#58; Kiwipete
Perft 1             48    0.000s    0.00Mnps   0.00%              0
Perft 2           2039    0.000s  185.36Mnps  29.52%            602
Perft 3          97862    0.000s  881.64Mnps  41.60%          40715
Perft 4        4085603    0.005s  823.05Mnps  29.75%        1215301
Perft 5      193690690    0.196s  986.88Mnps  40.41%       78261264
Perft 6     8031647685    9.576s  838.74Mnps  29.85%     2397800143
Perft 7   374190009323  376.537s  993.77Mnps  39.20%   146681492105

CPW 3&#58; En-passant traps FEN="8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -"
Perft 1             14    0.000s    0.00Mnps   0.00%              0
Perft 2            191    0.000s   12.73Mnps  15.71%             30
Perft 3           2812    0.000s   72.10Mnps   1.00%             28
Perft 4          43238    0.000s  107.02Mnps  31.00%          13402
Perft 5         674624    0.004s  159.49Mnps  21.82%         147175
Perft 6       11030083    0.038s  287.98Mnps  35.42%        3906448
Perft 7      178633661    0.565s  316.18Mnps  31.56%       56373334
Perft 8     3009794393    9.220s  326.45Mnps  37.36%     1124315382
Perft 9    50086749815  145.809s  343.51Mnps  35.26%    17658314868
When using 4GiB hash (64 bit key, replace if depth >= draft) I get these results:

Code: Select all

CPW 1&#58; Starting position
Perft 1             20    0.000s    0.00Mnps   0.00%              0
Perft 2            400    0.000s    1.65Mnps 100.00%            400
Perft 3           8902    0.005s    1.78Mnps  92.96%           8275
Perft 4         197281    0.052s    3.83Mnps  87.40%         172428
Perft 5        4865609    0.202s   24.05Mnps  48.48%        2358884
Perft 6      119060324    0.063s 1882.35Mnps  28.00%       33338865
Perft 7     3195901860    0.719s 4443.23Mnps  11.86%      379027132
Perft 8    84998978956    9.070s 9371.41Mnps   5.34%     4535770806
Perft 9  2439530234167  114.451s 21315.01Mnps   2.07%    50610404061
Perft10 69352859712417 1756.701s 39479.03Mnps   1.11%   773050676580
Perft11 2097651003696806 48505.039s 43246.04Mnps   0.99% 20752317250614

CPW 2&#58; Kiwipete
Perft 1             48    0.000s    0.00Mnps   0.00%              0
Perft 2           2039    0.000s    8.00Mnps  29.52%            602
Perft 3          97862    0.011s    9.06Mnps  41.60%          40715
Perft 4        4085603    0.171s   23.93Mnps  29.73%        1214553
Perft 5      193690690    0.350s  552.90Mnps  23.56%       45638917
Perft 6     8031647685    3.380s 2376.15Mnps   9.88%      793740492
Perft 7   374190009323   59.263s 6314.03Mnps   5.54%    20739894364
Perft 8 15493944087984 1340.522s 11558.15Mnps   1.95%   302161673442
PS. There is (at least) one position gperft by Paul Byrne beats my program:
CPW 3: gperft takes 139.764 seconds, mine 145.809s
Aleks Peshkov
Posts: 892
Joined: Sun Nov 19, 2006 9:16 pm
Location: Russia

Re: Perft using nullmove

Post by Aleks Peshkov »

Wow! Interesting direction of view!

My engine generates attack bitboards for each piece (and then legal moves bitboards from this attacks for each piece separately).

In my case it is possible to detect distinct affected pieces and generate moves only for them, keeping the relevant position difference "in mind". So even non free moves could skip copy-make stage completely.
ibid
Posts: 89
Joined: Mon Jun 13, 2011 12:09 pm

Re: Perft using nullmove

Post by ibid »

Lasse Hansen wrote:PS. There is (at least) one position gperft by Paul Byrne beats my program:
CPW 3: gperft takes 139.764 seconds, mine 145.809s
I'm glad such a position still exists! :)

I've been playing with a number of ways of doing nullmove-based perfts, at present I am going about it in a somewhat different way from you (assuming my quick scan of your code is semi-accurate!)

Instead of simply doing a nullmove perft and then looking for moves which will not change the perft value, I am storing extra information in the nullmove perft function. For example, the number of legal moves of each rook, bishop and queen. I then iterate though the moves and decide for each move which pieces need to be recomputed and which are guaranteed to be the same as the nullmove case. There is a balancing act there, an accurate test for a rook is as slow as simply recomputing the rook. A very quick test misses a lot of cases and recomputes too many positions.

In any case, I have played with your approach also, and, at least for me, it depends heavily on the type of position. When many moves do not change the perft value at all, the approach you are using does well. In other cases there are not many moves where the nullmove perft value can just be used as is. In these cases, my current approach can sometimes use bits and pieces of the nullmove perft computation.

-paul