summaryrefslogtreecommitdiff
path: root/src/testrecogn.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-25 08:44:25 -0700
committerScott Gasch <[email protected]>2026-09-25 08:44:25 -0700
commit9b28080f8b3dca4588cb2aaaf748451f45e3c626 (patch)
tree54e0aff5d6ee1520c590c7c04a5ae2f8299e7497 /src/testrecogn.c
parent66740dc89c7351c120c25788d0079055034f4942 (diff)
Two ideas:
1. Nullmove R=3 + depth/6 along with some ShouldWeDoNullmoveSearch tweaking and calibrating. 2. Probe EGTB everywhere in the search as long as the table/position is resident in memory.
Diffstat (limited to 'src/testrecogn.c')
-rw-r--r--src/testrecogn.c273
1 files changed, 267 insertions, 6 deletions
diff --git a/src/testrecogn.c b/src/testrecogn.c
index b64643e..10ee889 100644
--- a/src/testrecogn.c
+++ b/src/testrecogn.c
@@ -295,7 +295,7 @@ _TestRecognFenCases(IN OUT SEARCHER_THREAD_CONTEXT *ctx, IN OUT POSITION *pos)
}
ReInitializeSearcherContext(pos, ctx);
- uVal = RecognLookup(ctx, &iScore, FALSE);
+ uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER);
if ((UNRECOGNIZED != uVal) &&
((iScore <= -NMATE) || (iScore >= NMATE)))
{
@@ -387,7 +387,7 @@ Return value:
uPawnB, uKnightB, uBishopB);
ReInitializeSearcherContext(&pos, ctx);
- uVal = RecognLookup(ctx, &iScore, FALSE);
+ uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER);
if (UNRECOGNIZED == uVal)
{
uUnrecognized++;
@@ -624,6 +624,267 @@ Return value:
}
//
+// Build a KQKP (one queen, one pawn) position directly from square
+// placements. fBlackHasQueen selects which color plays the queen side
+// (cStrongKing/cWeakKing name roles, not colors, in both cases) -- see
+// TestRecognExhaustiveKQKP's header comment for why both assignments
+// are enumerated explicitly rather than assumed symmetric.
+//
+static FLAG
+_BuildKQKPPosition(OUT POSITION *pos,
+ IN COOR cStrongKing, IN COOR cWeakKing,
+ IN COOR cQueen, IN COOR cPawn,
+ IN ULONG uToMove, IN FLAG fBlackHasQueen)
+{
+ PIECE pBoard[128];
+ CHAR szFen[256];
+ CHAR *p;
+ ULONG x;
+
+ memset(pBoard, 0, sizeof(pBoard));
+ if (fBlackHasQueen)
+ {
+ pBoard[cStrongKing] = BLACK_KING;
+ pBoard[cWeakKing] = WHITE_KING;
+ pBoard[cQueen] = BLACK_QUEEN;
+ pBoard[cPawn] = WHITE_PAWN;
+ }
+ else
+ {
+ pBoard[cStrongKing] = WHITE_KING;
+ pBoard[cWeakKing] = BLACK_KING;
+ pBoard[cQueen] = WHITE_QUEEN;
+ pBoard[cPawn] = BLACK_PAWN;
+ }
+
+ memset(szFen, 0, sizeof(szFen));
+ p = szFen;
+ for (x = 0; x < 120; x++)
+ {
+ if ((x % 15 == 0) && (x != 0))
+ {
+ *p++ = '/';
+ }
+ if (!IS_ON_BOARD(x)) continue;
+ if (0 == pBoard[x])
+ {
+ *p++ = '1';
+ }
+ else
+ {
+ *p++ = *(PieceAbbrev(pBoard[x]) + 1);
+ }
+ }
+ *p++ = ' ';
+ *p++ = (WHITE == uToMove) ? 'w' : 'b';
+ strcat(p, " - - 0 1");
+
+ if (FALSE == FenToPosition(pos, szFen))
+ {
+ return(FALSE);
+ }
+ if (InCheck(pos, FLIP(pos->uToMove)))
+ {
+ return(FALSE);
+ }
+ return(TRUE);
+}
+
+//
+// Does the side to move in ctx's current position have at least one
+// legal move? Used only to classify a mismatch as "the to-move side is
+// simply stalemated" vs. something else -- not part of the recognizer
+// itself. Mirrors root.c's _DetectPreMoveTerminalStates legal-move
+// count idiom (GenerateMoves then MakeMove/UnmakeMove-filter), just
+// short-circuiting on the first legal move found instead of counting
+// them all.
+//
+static FLAG
+_ToMoveHasLegalMove(IN OUT SEARCHER_THREAD_CONTEXT *ctx)
+{
+ ULONG x;
+ MOVE mv;
+ FLAG fInCheck = InCheck(&ctx->sPosition, ctx->sPosition.uToMove);
+
+ GenerateMoves(ctx, NULLMOVE,
+ fInCheck ? GENERATE_ESCAPES : GENERATE_ALL_MOVES);
+ for (x = ctx->sMoveStack.uBegin[ctx->uPly];
+ x < ctx->sMoveStack.uEnd[ctx->uPly];
+ x++)
+ {
+ mv = ctx->sMoveStack.mvf[x].mv;
+ if (TRUE == MakeMove(ctx, mv))
+ {
+ UnmakeMove(ctx, mv);
+ return(TRUE);
+ }
+ }
+ return(FALSE);
+}
+
+void
+TestRecognExhaustiveKQKP(void)
+/**
+
+Routine description:
+
+ Exhaustively enumerate every legal KQKP (one queen, one pawn)
+ position -- not a random sample -- and verify _RecognizeKQKP's
+ verdict against direct EGTB probes wherever coverage exists. Same
+ "no counterexamples in N random samples is false confidence"
+ rationale as TestRecognExhaustiveKNKP/KPK -- doubly warranted here
+ since _RecognizeKQKP's win/UNRECOGNIZED boundary (rook/bishop pawn
+ on the square before queening) was scoped down from memory of
+ classical endgame theory rather than derived from EGTB data, and
+ this file has real precedent (_RecognizeKBKP's 2026-09-18 history)
+ for a plausible-looking classical shortcut turning out unsound once
+ fuzzed.
+
+ Learned from TestRecognExhaustiveKPK's own 2026-09-18 history:
+ fixing which COLOR plays the strong side is NOT a safe assumption
+ in this codebase, even though RecognLookup/_RecognizeKQKP determine
+ "strong side" dynamically from piece counts -- iterates both
+ fBlackHasQueen assignments explicitly rather than trusting that
+ symmetry. Unlike TestRecognExhaustiveKPK, this keeps the left/right
+ mirror-halving restriction (FILE(cStrongKing) <= D, same as
+ TestRecognExhaustiveKNKP) -- safe here because, unlike _RecognizeKPK's
+ hand-typed critical-square table, _RecognizeKQKP contains no
+ per-square lookup data at all: its only file-dependent test is
+ "FILE(cPawn) == A/C/F/H", which treats both rook files and both
+ bishop files identically by construction, so a left/right board
+ reflection cannot expose an asymmetric transcription bug the way it
+ did for KPK's table.
+
+ Not wired into TestRecogn()'s automatic startup suite (same
+ precommit_check.sh budget reasoning as KNKP/KPK) -- call this on
+ demand when validating a KQKP-area change.
+
+Parameters:
+
+ void
+
+Return value:
+
+ void
+
+**/
+{
+ SEARCHER_THREAD_CONTEXT *ctx;
+ POSITION pos;
+ COOR cStrongKing, cWeakKing, cQueen, cPawn;
+ ULONG uToMove;
+ SCORE iScore, iEgtb;
+ ULONG uVal;
+ ULONG uTotal = 0, uChecked = 0, uSkipped = 0, uWrong = 0;
+ ULONG uReportedWrong = 0;
+ ULONG uWrongStalemated = 0;
+ FLAG fStalemated;
+
+ Trace("Exhaustive KQKP (1 queen, 1 pawn) verification against "
+ "EGTB ground truth -- this takes a while...\n");
+ ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx)
+ {
+ return;
+ }
+ InitializeSearcherContext(&pos, ctx);
+
+ FOREACH_SQUARE(cStrongKing)
+ {
+ if (!IS_ON_BOARD(cStrongKing)) continue;
+ if (FILE(cStrongKing) > D) continue;
+
+ FOREACH_SQUARE(cWeakKing)
+ {
+ if (!IS_ON_BOARD(cWeakKing)) continue;
+ if (DISTANCE(cStrongKing, cWeakKing) < 2) continue;
+
+ FOREACH_SQUARE(cQueen)
+ {
+ if (!IS_ON_BOARD(cQueen)) continue;
+ if ((cQueen == cStrongKing) || (cQueen == cWeakKing)) continue;
+
+ FOREACH_SQUARE(cPawn)
+ {
+ if (!IS_ON_BOARD(cPawn)) continue;
+ if (RANK1(cPawn) || RANK8(cPawn)) continue;
+ if ((cPawn == cStrongKing) || (cPawn == cWeakKing) ||
+ (cPawn == cQueen)) continue;
+
+ for (uToMove = BLACK; uToMove <= WHITE; uToMove++)
+ {
+ ULONG uColorPass;
+
+ for (uColorPass = 0; uColorPass < 2; uColorPass++)
+ {
+ uTotal++;
+ if (0 == (uTotal % 500000))
+ {
+ Trace(" ... %lu raw combos so far (%lu checked, "
+ "%lu wrong)\n", uTotal, uChecked, uWrong);
+ }
+ if (FALSE == _BuildKQKPPosition(&pos, cStrongKing,
+ cWeakKing, cQueen,
+ cPawn, uToMove,
+ (1 == uColorPass)))
+ {
+ continue;
+ }
+ ReInitializeSearcherContext(&pos, ctx);
+ uVal = _RecognizeKQKP(ctx, &iScore);
+ if (UNRECOGNIZED == uVal)
+ {
+ continue;
+ }
+ if (FALSE == ProbeEGTB(ctx, &iEgtb))
+ {
+ uSkipped++;
+ continue;
+ }
+ uChecked++;
+ if (FALSE == _RecognizerAgreesWithEGTB(iScore, uVal,
+ iEgtb))
+ {
+ uWrong++;
+ fStalemated =
+ (FALSE == InCheck(&ctx->sPosition,
+ ctx->sPosition.uToMove)) &&
+ (FALSE == _ToMoveHasLegalMove(ctx));
+ if (TRUE == fStalemated)
+ {
+ uWrongStalemated++;
+ }
+ if (uReportedWrong < 20)
+ {
+ Trace("MISMATCH (blackHasQueen=%lu) iScore=%d "
+ "uVal=%lu iEgtb=%d toMove=%lu "
+ "stalemated=%lu\n",
+ uColorPass, iScore, uVal, iEgtb,
+ pos.uToMove, fStalemated);
+ DumpPosition(&pos);
+ uReportedWrong++;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ {
+ ULONG uWrongOther = uWrong - uWrongStalemated;
+
+ free(ctx);
+ Trace("Exhaustive KQKP: %lu raw combos, %lu checked against EGTB, "
+ "%lu skipped (no EGTB coverage), %lu WRONG (%lu of those are "
+ "the weak side simply stalemated, %lu are something else).\n",
+ uTotal, uChecked, uSkipped, uWrong, uWrongStalemated,
+ uWrongOther);
+ }
+}
+
+//
// Build a KPK (one pawn, one lone king) position directly from square
// placements. fBlackHasPawn selects which color plays the pawn side
// (cStrongKing/cWeakKing name roles, not colors, in both cases).
@@ -845,7 +1106,7 @@ Return value:
continue;
}
ReInitializeSearcherContext(&pos, ctx);
- uVal = RecognLookup(ctx, &iScore, FALSE);
+ uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER);
if (UNRECOGNIZED == uVal)
{
continue;
@@ -1227,7 +1488,7 @@ Return value:
continue;
}
ReInitializeSearcherContext(&pos, ctx);
- uVal = RecognLookup(ctx, &iScore, FALSE);
+ uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER);
if (UNRECOGNIZED == uVal)
{
uUnrecognized++;
@@ -1758,7 +2019,7 @@ Return value:
_MakeRandomEndgame(&pos, uPawnA, 0, 0, uPawnB, 0, 0);
ReInitializeSearcherContext(&pos, ctx);
- uVal = RecognLookup(ctx, &iScore, FALSE);
+ uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER);
if (UNRECOGNIZED == uVal)
{
uUnrecognized++;
@@ -2011,7 +2272,7 @@ Return value:
// signature routing a real search would use.
//
{
- ULONG uVal = RecognLookup(ctx, &iRecScore, FALSE);
+ ULONG uVal = RecognLookup(ctx, &iRecScore, EGTB_PROBE_NEVER);
if (UNRECOGNIZED != uVal)
{
uRecognClaimed++;