Skip to content

How to Navigate Variation Trees with Cursors

This guide demonstrates how to traverse branching game trees using scid_game_cursor handles in the libscid C ABI, stepping forward and backward through plies, entering sub-variations, and inspecting moves, comments, and NAGs at each position.


1. Overview of Symbols

  • scid_game_cursor_create: Spawns a cursor at the root position of a game.
  • scid_game_cursor_next: Steps forward along the active variation, producing a new cursor pointing to the next move.
  • scid_game_cursor_previous: Steps backward toward the root position.
  • scid_game_cursor_variation_count_get: Returns the number of sub-variations diverging from the current position.
  • scid_game_cursor_variation_enter: Enters a specified sub-variation at a given 0-based index.
  • scid_game_cursor_variation_leave: Ascends out of the current sub-variation back to the parent variation.
  • scid_game_cursor_next_move_san_get: Inspects the Standard Algebraic Notation (SAN) string of the upcoming move without stepping forward.
  • scid_game_cursor_next_movespec_get: Retrieves the low-level scid_movespec descriptor of the upcoming move.

2. Complete Recipe

#include <scid/scid.h>

#include <stdio.h>
#include <string.h>

static int
check(
    scid_error  error,
    const char* call)
{
    if (error == SCID_OK)
    {
        return 1;
    }

    fprintf(stderr, "%s failed with scid_error %hu\n", call, error);
    return 0;
}


static int
take_cursor(
    scid_game_cursor** cursor,
    scid_game_cursor** next_cursor)
{
    if (next_cursor == NULL || *next_cursor == NULL)
    {
        return 0;
    }

    scid_game_cursor_free(*cursor);
    *cursor = *next_cursor;
    *next_cursor = NULL;
    return 1;
}


static int
read_text(
    scid_error  error,
    const char* call,
    const char* label,
    const char* text,
    size_t      text_size)
{
    if (!check(error, call))
    {
        return 0;
    }

    printf("%s%.*s\n", label, (int)text_size, text);
    return 1;
}


static int
text_equals(
    const char* text,
    size_t      text_size,
    const char* expected)
{
    return text_size == strlen(expected) && strncmp(text, expected, text_size) == 0;
}


static int
print_next_move(scid_game_cursor* cursor)
{
    char          san[64];
    char          uci[16];
    char          comment[256];
    scid_movespec move;
    scid_nag      nag = 0;
    size_t        san_size = 0;
    size_t        uci_size = 0;
    size_t        comment_size = 0;
    size_t        nag_count = 0;
    size_t        variation_count = 0;
    size_t        ply = 0;

    if (!check(scid_game_cursor_ply_get(cursor, &ply), "scid_game_cursor_ply_get") ||
        !check(
            scid_game_cursor_next_movespec_get(cursor, &move),
            "scid_game_cursor_next_movespec_get") ||
        !read_text(
            scid_movespec_to_uci(move, uci, sizeof(uci), &uci_size), "scid_movespec_to_uci",
            "next uci: ", uci, uci_size) ||
        !read_text(
            scid_game_cursor_next_move_san_get(cursor, san, sizeof(san), &san_size),
            "scid_game_cursor_next_move_san_get", "next san: ", san, san_size) ||
        !read_text(
            scid_game_cursor_next_move_comment_get(cursor, comment, sizeof(comment), &comment_size),
            "scid_game_cursor_next_move_comment_get", "next comment: ", comment, comment_size) ||
        !check(
            scid_game_cursor_next_move_nag_count_get(cursor, &nag_count),
            "scid_game_cursor_next_move_nag_count_get") ||
        !check(
            scid_game_cursor_variation_count_get(cursor, &variation_count),
            "scid_game_cursor_variation_count_get"))
    {
        return 0;
    }

    if (!text_equals(san, san_size, "e4") || !text_equals(uci, uci_size, "e2e4") ||
        !text_equals(comment, comment_size, "King pawn") || ply != 0 || nag_count != 1 ||
        variation_count != 1)
    {
        return 0;
    }

    printf("ply before move: %zu\n", ply);
    printf("next nag count: %zu\n", nag_count);
    printf("next variation count: %zu\n", variation_count);

    if (nag_count > 0)
    {
        if (!check(
                scid_game_cursor_next_move_nag_at_get(cursor, 0, &nag),
                "scid_game_cursor_next_move_nag_at_get"))
        {
            return 0;
        }
        printf("first next nag: %u\n", (unsigned)nag);
    }

    return 1;
}


int
main(void)
{
    const char*       start_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
    const char*       pgn = "[Event \"Navigation\"]\n"
                            "[Result \"*\"]\n"
                            "\n"
                            "{Before game} 1. e4 $1 {King pawn} "
                            "({Queen pawn line} 1. d4 {Queen pawn} d5) e5 2. Nf3 Nc6 *\n";
    scid_game*        game = NULL;
    scid_game_cursor* cursor = NULL;
    scid_game_cursor* next_cursor = NULL;
    scid_position*    position = NULL;
    char              diagnostic[1024];
    char              text[256];
    scid_movespec     move;
    int               moved = 0;
    size_t            diagnostic_size = 0;
    size_t            text_size = 0;
    size_t            depth = 0;
    size_t            variation_count = 0;

    if (!check(
            scid_position_create_from_fen(start_fen, &position), "scid_position_create_from_fen") ||
        !check(
            scid_game_create(
                position, pgn, strlen(pgn), &game, diagnostic, sizeof(diagnostic),
                &diagnostic_size),
            "scid_game_create") ||
        !check(scid_game_cursor_create(game, &cursor), "scid_game_cursor_create"))
    {
        fprintf(stderr, "%.*s\n", (int)diagnostic_size, diagnostic);
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }


    if (!read_text(
            scid_game_cursor_comment_get(cursor, text, sizeof(text), &text_size),
            "scid_game_cursor_comment_get", "initial comment: ", text, text_size) ||
        !text_equals(text, text_size, "Before game") || !print_next_move(cursor) ||
        !check(
            scid_game_cursor_variation_count_get(cursor, &variation_count),
            "scid_game_cursor_variation_count_get") ||
        variation_count != 1)
    {
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }


    if (!check(
            scid_game_cursor_variation_enter(cursor, 0, &moved, &next_cursor),
            "scid_game_cursor_variation_enter") ||
        !moved || !take_cursor(&cursor, &next_cursor) ||
        !check(
            scid_game_cursor_variation_depth_get(cursor, &depth),
            "scid_game_cursor_variation_depth_get") ||
        depth != 1 ||
        !read_text(
            scid_game_cursor_comment_get(cursor, text, sizeof(text), &text_size),
            "scid_game_cursor_comment_get", "variation comment: ", text, text_size) ||
        !text_equals(text, text_size, "Queen pawn line") ||
        !read_text(
            scid_game_cursor_next_move_san_get(cursor, text, sizeof(text), &text_size),
            "scid_game_cursor_next_move_san_get", "variation first move: ", text, text_size) ||
        !text_equals(text, text_size, "d4"))
    {
        scid_game_cursor_free(next_cursor);
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }
    next_cursor = NULL;

    if (!check(
            scid_game_cursor_variation_exit(cursor, &moved, &next_cursor),
            "scid_game_cursor_variation_exit") ||
        !moved || !take_cursor(&cursor, &next_cursor))
    {
        scid_game_cursor_free(next_cursor);
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }
    next_cursor = NULL;

    if (!check(scid_game_cursor_next(cursor, &moved, &next_cursor), "scid_game_cursor_next") ||
        !moved || !take_cursor(&cursor, &next_cursor) ||
        !read_text(
            scid_game_cursor_previous_move_san_get(cursor, text, sizeof(text), &text_size),
            "scid_game_cursor_previous_move_san_get", "previous san: ", text, text_size) ||
        !text_equals(text, text_size, "e4") ||
        !check(
            scid_game_cursor_previous_movespec_get(cursor, &move),
            "scid_game_cursor_previous_movespec_get") ||
        !read_text(
            scid_movespec_to_uci(move, text, sizeof(text), &text_size), "scid_movespec_to_uci",
            "previous uci: ", text, text_size) ||
        !text_equals(text, text_size, "e2e4") ||
        !read_text(
            scid_game_cursor_next_move_san_get(cursor, text, sizeof(text), &text_size),
            "scid_game_cursor_next_move_san_get", "next san after e4: ", text, text_size) ||
        !text_equals(text, text_size, "e5") ||
        !check(
            scid_game_cursor_next_movespec_get(cursor, &move),
            "scid_game_cursor_next_movespec_get") ||
        !read_text(
            scid_movespec_to_uci(move, text, sizeof(text), &text_size), "scid_movespec_to_uci",
            "next uci after e4: ", text, text_size) ||
        !text_equals(text, text_size, "e7e5"))
    {
        scid_game_cursor_free(next_cursor);
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }
    next_cursor = NULL;

    scid_position_free(position);
    scid_game_cursor_free(cursor);
    scid_game_free(game);
    return 0;
}

3. Key Concepts and Patterns

Cursor Immutability and Variation Traversal

Navigation cursors in libscid never mutate internal state in place. Every navigational call (scid_game_cursor_next, scid_game_cursor_variation_enter, etc.) accepts an output pointer scid_game_cursor** out_cursor to return a newly allocated cursor:

scid_game_cursor* cursor = NULL;
scid_game_cursor_create(game, &cursor);

/* Inspect variations available at current position */
size_t variation_count = 0;
scid_game_cursor_variation_count_get(cursor, &variation_count);

if (variation_count > 0)
{
    scid_game_cursor* var_cursor = NULL;
    /* Enter variation 0 */
    scid_game_cursor_variation_enter(cursor, 0, &var_cursor);

    /* ... navigate within variation ... */

    scid_game_cursor_free(var_cursor);
}

scid_game_cursor_free(cursor);