Skip to content

How to Mutate and Merge Move Trees

This guide explains how to structurally alter chess game trees using the libscid C ABI: truncating variations at a specific ply, deleting unwanted sub-variations, and merging moves from an external source game into an existing target game.


1. Overview of Symbols

  • scid_game_cursor_truncate: Removes all moves following the cursor along the current variation, cutting the tree short at the active ply.
  • scid_game_cursor_variation_delete: Deletes a specific sub-variation branching from the current position.
  • scid_game_merge_moves: Merges moves, variations, comments, and NAGs from a source game into a target game starting at the active cursor position.

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
contains(
    const char* text,
    const char* needle)
{
    return strstr(text, needle) != NULL;
}


int
main(void)
{
    const char*       start_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
    const char*       pgn = "[Event \"Mutation\"]\n"
                            "[Result \"*\"]\n"
                            "\n"
                            "1. e4 e5 (1... c5) *\n";
    scid_game*        game = NULL;
    scid_game*        source_game = NULL;
    scid_game_cursor* cursor = NULL;
    scid_game_cursor* next_cursor = NULL;
    scid_game_cursor* source_cursor = NULL;
    scid_game_cursor* source_next_cursor = NULL;
    scid_position*    position = NULL;
    scid_movespec     move;
    char              output[4096];
    int               changed = 0;
    int               moved = 0;
    size_t            output_size = 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, NULL, 0, NULL),
            "scid_game_create") ||
        !check(scid_game_cursor_create(game, &cursor), "scid_game_cursor_create"))
    {
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }

    if (!check(scid_game_cursor_next(cursor, &moved, &next_cursor), "scid_game_cursor_next") ||
        !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_comment_set(game, cursor, "King pawn"),
            "scid_game_cursor_comment_set") ||
        !check(scid_game_cursor_nag_add(game, cursor, 1, &changed), "scid_game_cursor_nag_add") ||
        !changed ||
        !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_position_get(cursor, position), "scid_game_cursor_position_get") ||
        !check(
            scid_movespec_create_from_san(position, "e6", &move),
            "scid_movespec_create_from_san") ||
        !check(scid_game_create_blank(position, &source_game), "scid_game_create_blank") ||
        !check(scid_game_cursor_create(source_game, &source_cursor), "scid_game_cursor_create") ||
        !check(
            scid_game_cursor_comment_set(source_game, source_cursor, "French branch"),
            "scid_game_cursor_comment_set") ||
        !check(
            scid_game_cursor_move_add(source_game, source_cursor, move, &source_next_cursor),
            "scid_game_cursor_move_add") ||
        !take_cursor(&source_cursor, &source_next_cursor) ||
        !check(
            scid_game_merge_moves(
                game, cursor, source_game, SCID_GAME_MERGE_MOVES_INSERT_VARIATION, &next_cursor),
            "scid_game_merge_moves") ||
        !take_cursor(&cursor, &next_cursor))
    {
        scid_game_cursor_free(next_cursor);
        scid_game_cursor_free(source_next_cursor);
        scid_game_cursor_free(source_cursor);
        scid_game_free(source_game);
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }
    scid_game_cursor_free(source_cursor);
    source_cursor = NULL;
    scid_game_cursor_free(source_next_cursor);
    source_next_cursor = NULL;
    scid_game_free(source_game);
    source_game = NULL;
    next_cursor = NULL;

    if (!check(
            scid_game_cursor_variation_promote_to_first(game, cursor, &changed, &next_cursor),
            "scid_game_cursor_variation_promote_to_first") ||
        !changed || !take_cursor(&cursor, &next_cursor))
    {
        scid_game_cursor_free(next_cursor);
        scid_game_cursor_free(source_next_cursor);
        scid_game_cursor_free(source_cursor);
        scid_game_free(source_game);
        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) ||
        !check(
            scid_game_to_pgn(game, NULL, output, sizeof(output), &output_size), "scid_game_to_pgn"))
    {
        scid_game_cursor_free(next_cursor);
        scid_game_cursor_free(source_next_cursor);
        scid_game_cursor_free(source_cursor);
        scid_game_free(source_game);
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }
    next_cursor = NULL;

    printf("%.*s", (int)output_size, output);

    if (!contains(output, "e4 $1 {King pawn}") || !contains(output, "{French branch}") ||
        !contains(output, "e6") || !contains(output, "c5"))
    {
        scid_position_free(position);
        scid_game_cursor_free(cursor);
        scid_game_free(game);
        return 1;
    }

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

3. Key Concepts and Patterns

Truncating Plies

To slice off moves after a given board position, position a cursor at the desired terminal move and invoke scid_game_cursor_truncate:

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

/* Navigate to ply 2 */
scid_game_cursor* next_cursor = NULL;
scid_game_cursor_next(cursor, &next_cursor);
take_cursor(&cursor, &next_cursor);

/* Truncate everything after this move */
scid_game_cursor_truncate(cursor);

Merging Independent Games

The scid_game_merge_moves function combines variation trees, grafting alternative lines of play as variations if the mainline diverges:

scid_game* target_game = NULL;
scid_game* source_game = NULL;

/* Merge source_game into target_game at target_cursor position */
scid_game_merge_moves(target_game, target_cursor, source_game, source_cursor);