/** Copyright (c) Scott Gasch Module Name: dumptree.c Abstract: TREEDUMP: a flat, line-oriented, path-keyed node/move trace of the search tree, meant to be diffed against an identically-instrumented Stockfish build on the same FEN. See TREEDUMP_FORMAT.md for the output format and chess.h's TREEDUMP comment block for the calling convention. Build-time-only ("gmake TREEDUMP=1 ..."), always-on-when-built -- there is no runtime toggle/filter. Keep a dump small by using a shallow "sd N"/"go depth N", not by restricting what gets written. This replaces an earlier (2004), long-unused tree-dump mechanism that wrote XML for a Windows GUI (typhoonui.exe) long since gone; that code and its DUMP_TREE build flag have been removed outright rather than kept alongside this one, on the assumption that a mechanism nothing has exercised in two decades is broken. Author: Scott Gasch (scott.gasch@gmail.com) 17 Jul 2004, replaced 2026-10-22 **/ #ifdef TREEDUMP #include "chess.h" static FILE *g_pfTreeDump = NULL; void TDOpen(void) /** Routine description: Open (truncate) the fixed-name TREEDUMP output file. Call once at startup when built with TREEDUMP -- there is no runtime toggle, so this always runs and the file always gets every node/move of every search performed. fflush() after every single line (unbuffered) since TREEDUMP runs are expected to be small (a shallow sd/go depth), not performance sensitive, and a flushed file is immediately diffable even if the engine is killed mid-run or crashes. Parameters: void Return value: void **/ { g_pfTreeDump = fopen("treedump.log", "w"); if (NULL == g_pfTreeDump) { UtilPanic(FATAL_ACCESS_DENIED, NULL, "treedump.log", "write", "NULL", __FILE__, __LINE__); } } void TDClose(void) /** Routine description: Flush and close the TREEDUMP output file. Parameters: void Return value: void **/ { if (NULL != g_pfTreeDump) { fflush(g_pfTreeDump); fclose(g_pfTreeDump); g_pfTreeDump = NULL; } } // Formats a move as a UCI/ICS-style coordinate string ("e2e4"), or the // literal "0000" for a null move (MOVE{0}, same sentinel as NULLMOVE in // data.c) -- matches Stockfish's UCIEngine::move()'s spelling for // Move::null() exactly, so the two engines' logs tokenize identically. // Not thread safe (static buffer, same caveat as MoveToIcs itself, // which this wraps) -- TREEDUMP builds are meant for single-threaded // diagnostic runs only, matching this project's own comparison // methodology (see CLAUDE.md's "--cpus 1" convention). static CHAR * _TDMoveToStr(MOVE mv) { static CHAR szNull[] = "0000"; CHAR *p; if (0 == mv.uMove) { return(szNull); } p = MoveToIcs(mv); ASSERT(p); return(p); } // Builds the path of moves from the root up to and including ctx's // current ply's own incoming move -- i.e. ctx->sPlyInfo[0..ctx->uPly-1]. // MakeMove() (move.c) stores each move at the OLD ply index before // incrementing ctx->uPly, so this naturally includes: // - the move just made by a caller logging from inside SearchOneMove // (called after MakeMove, before UnmakeMove -- ctx->uPly is already // bumped to the child ply, so sPlyInfo[childPly - 1] == that move), or // - only the moves leading INTO this node, with none of this node's // own not-yet-tried moves, for a caller logging a node's own RESULT // from its own ply (e.g. search.c's "end:"). // Same role as the Stockfish side's TreeDump::path() (search.cpp) -- // see TREEDUMP_FORMAT.md. Not thread safe (static buffer); see // _TDMoveToStr's comment. static CHAR * _TDPath(SEARCHER_THREAD_CONTEXT *ctx) { static CHAR szPath[2048]; ULONG p; szPath[0] = '\0'; for (p = 0; p < ctx->uPly; p++) { if ('\0' != szPath[0]) { strncat(szPath, " ", ARRAY_LENGTH(szPath) - strlen(szPath) - 1); } strncat(szPath, _TDMoveToStr(ctx->sPlyInfo[p].mv), ARRAY_LENGTH(szPath) - strlen(szPath) - 1); } return(szPath); } void TDMove(SEARCHER_THREAD_CONTEXT *ctx, ULONG uDepth, SCORE iAlpha, SCORE iBeta, CHAR *szKind, CHAR *szAct, double dReducedPlies, double dExtendedPlies, SCORE iScore, FLAG fSkipped) /** Routine description: Log one MOVE line: a decision about the move already made at ctx's current (child) ply -- see this file's header comment for the calling convention (after MakeMove, before UnmakeMove). Parameters: SEARCHER_THREAD_CONTEXT *ctx : current searcher thread context, already past MakeMove for the move being logged. ULONG uDepth : remaining depth (in internal ONE_PLY ticks) at the PARENT node, before this move's own extension/ reduction. SCORE iAlpha, iBeta : the parent node's alpha/beta. CHAR *szKind : move classification, e.g. "FIRST", "CAP", "QUIET", "NULL", freeform. CHAR *szAct : what happened to it, e.g. "FULL", "LMR", "LMP", "NMP", freeform. double dReducedPlies, dExtendedPlies : magnitude, in whole plies, of any reduction/extension applied (0.0 if none). SCORE iScore : the score returned by searching this move; ignored (but still required as a parameter for a uniform call shape) when fSkipped is TRUE. FLAG fSkipped : TRUE if this move was pruned/skipped without being searched at all (e.g. LMP) -- logs "score=SKIPPED" instead of a numeric score. Return value: void **/ { if (NULL == g_pfTreeDump) { return; } if (TRUE == fSkipped) { fprintf(g_pfTreeDump, "MOVE path=\"%s\" depth=%.2f alpha=%d beta=%d kind=%s " "act=%s r=%.2f ext=%.2f score=SKIPPED\n", _TDPath(ctx), (double)uDepth / ONE_PLY, iAlpha, iBeta, szKind, szAct, dReducedPlies, dExtendedPlies); } else { fprintf(g_pfTreeDump, "MOVE path=\"%s\" depth=%.2f alpha=%d beta=%d kind=%s " "act=%s r=%.2f ext=%.2f score=%d\n", _TDPath(ctx), (double)uDepth / ONE_PLY, iAlpha, iBeta, szKind, szAct, dReducedPlies, dExtendedPlies, iScore); } fflush(g_pfTreeDump); } void TDResult(SEARCHER_THREAD_CONTEXT *ctx, ULONG uDepth, SCORE iAlpha, SCORE iBeta, SCORE iEval, SCORE iBestScore) /** Routine description: Log one RESULT line: this node's own final outcome. Call from the node's own ply (i.e. before any further move has been made) -- see this file's header comment. Parameters: SEARCHER_THREAD_CONTEXT *ctx : current searcher thread context, at the node whose result is being logged. ULONG uDepth : remaining depth (internal ONE_PLY ticks) this node was searched to. SCORE iAlpha, iBeta : this node's ORIGINAL (pre-widening) alpha/beta -- callers should pass iInitialAlpha, not whatever iAlpha was mutated to internally, so the bound classification below means what it says. SCORE iEval : this node's static eval (0/stale if the node returned before Eval() ran -- diagnostic only). SCORE iBestScore : this node's final score. Return value: void **/ { CHAR *szBound; if (NULL == g_pfTreeDump) { return; } if (iBestScore >= iBeta) { szBound = "LOWER"; } else if (iBestScore <= iAlpha) { szBound = "UPPER"; } else { szBound = "EXACT"; } fprintf(g_pfTreeDump, "RESULT path=\"%s\" depth=%.2f alpha=%d beta=%d eval=%d " "bound=%s score=%d\n", _TDPath(ctx), (double)uDepth / ONE_PLY, iAlpha, iBeta, iEval, szBound, iBestScore); fflush(g_pfTreeDump); } #endif // TREEDUMP