How to Author Games with Moves, Comments, and Variations
This guide demonstrates how to construct a chess game programmatically through the libscid C ABI by creating a blank game, appending moves via immutable cursors, attaching comments and Numeric Annotation Glyphs (NAGs), creating branching variations, and configuring custom PGN export options.
1. Overview of Symbols
scid_game_create_blank: Constructs an empty game initialised to the standard start position or an arbitrary custom position.scid_game_cursor_create: Allocates an initial navigation cursor positioned at the root of the game tree.scid_game_cursor_move_add: Applies a move from the current cursor position, appending it to the game tree and producing a new cursor pointing to the resulting board state.scid_game_cursor_comment_set: Attaches an explanatory annotation or text comment to the move referenced by the cursor.scid_game_cursor_nag_add: Attaches a standard Numeric Annotation Glyph (e.g.$1for "!",$2for "?") to the move.scid_game_cursor_variation_add: Branches from the current move, creating an alternative line of play.scid_game_pgn_options_create: Allocates a configuration handle to control PGN export formatting (such as symbolic NAGs or omitting variations).scid_game_to_pgn: Serialises the complete game structure to PGN.
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
take_position(
scid_position** position,
scid_position* next_position)
{
if (next_position == NULL)
{
return 0;
}
scid_position_free(*position);
*position = next_position;
return 1;
}
static int
add_san_move(
scid_game* game,
scid_game_cursor** cursor,
scid_position** position,
const char* san)
{
scid_movespec move;
scid_game_cursor* next_cursor = NULL;
scid_position* next_position = NULL;
if (!check(
scid_movespec_create_from_san(*position, san, &move), "scid_movespec_create_from_san"))
{
return 0;
}
if (!check(
scid_game_cursor_move_add(game, *cursor, move, &next_cursor),
"scid_game_cursor_move_add") ||
!check(
scid_position_create_with_san(*position, san, &next_position),
"scid_position_create_with_san"))
{
scid_game_cursor_free(next_cursor);
scid_position_free(next_position);
return 0;
}
return take_cursor(cursor, &next_cursor) && take_position(position, next_position);
}
static int
contains(
const char* text,
const char* needle)
{
return strstr(text, needle) != NULL;
}
int
main(void)
{
const char* standard_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
scid_game* game = NULL;
scid_position* position = NULL;
scid_game_cursor* cursor = NULL;
scid_game_pgn_options* pgn_options = NULL;
char pgn[4096];
size_t pgn_size = 0;
int moved = 0;
int changed = 0;
if (!check(
scid_position_create_from_fen(standard_fen, &position),
"scid_position_create_from_fen") ||
!check(scid_game_create_blank(position, &game), "scid_game_create_blank") ||
!check(scid_game_cursor_create(game, &cursor), "scid_game_cursor_create"))
{
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
if (!check(scid_game_tag_set(game, "Event", "C ABI Example"), "scid_game_tag_set") ||
!check(scid_game_tag_set(game, "Site", "Example"), "scid_game_tag_set") ||
!check(scid_game_tag_set(game, "Date", "2026.06.10"), "scid_game_tag_set") ||
!check(scid_game_tag_set(game, "White", "White"), "scid_game_tag_set") ||
!check(scid_game_tag_set(game, "Black", "Black"), "scid_game_tag_set") ||
!check(scid_game_tag_set(game, "Result", "*"), "scid_game_tag_set"))
{
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
if (!add_san_move(game, &cursor, &position, "e4") ||
!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 || !add_san_move(game, &cursor, &position, "e5") ||
!add_san_move(game, &cursor, &position, "Nf3") ||
!add_san_move(game, &cursor, &position, "Nc6"))
{
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
{
scid_game_cursor* next_cursor = NULL;
if (!check(
scid_game_cursor_to_ply(cursor, 1, &moved, &next_cursor),
"scid_game_cursor_to_ply") ||
!moved || !take_cursor(&cursor, &next_cursor))
{
scid_game_cursor_free(next_cursor);
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
}
{
scid_game_cursor* next_cursor = NULL;
if (!check(
scid_game_cursor_position_get(cursor, position), "scid_game_cursor_position_get") ||
!check(
scid_game_cursor_variation_add(
game, cursor, "Sicilian branch", &changed, &next_cursor),
"scid_game_cursor_variation_add") ||
!changed || !take_cursor(&cursor, &next_cursor))
{
scid_game_cursor_free(next_cursor);
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
next_cursor = NULL;
if (!add_san_move(game, &cursor, &position, "c5"))
{
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
}
{
scid_game_cursor* 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_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
next_cursor = NULL;
if (!check(scid_game_cursor_to_end(cursor, &next_cursor), "scid_game_cursor_to_end") ||
!take_cursor(&cursor, &next_cursor))
{
scid_game_cursor_free(next_cursor);
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
}
if (!check(scid_game_to_pgn(game, NULL, pgn, sizeof(pgn), &pgn_size), "scid_game_to_pgn"))
{
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
printf("%.*s", (int)pgn_size, pgn);
if (!contains(pgn, "[Event \"C ABI Example\"]") || !contains(pgn, "e4 $1 {King pawn}") ||
!contains(pgn, "{Sicilian branch}") || !contains(pgn, "c5"))
{
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
if (!check(scid_game_pgn_options_create(&pgn_options), "scid_game_pgn_options_create") ||
!check(
scid_game_pgn_options_symbolic_nags_set(pgn_options, 1),
"scid_game_pgn_options_symbolic_nags_set") ||
!check(
scid_game_pgn_options_variations_set(pgn_options, 0),
"scid_game_pgn_options_variations_set") ||
!check(
scid_game_to_pgn(game, pgn_options, pgn, sizeof(pgn), &pgn_size), "scid_game_to_pgn"))
{
scid_game_pgn_options_free(pgn_options);
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
printf("\nMainline with symbolic NAGs:\n%.*s", (int)pgn_size, pgn);
if (!contains(pgn, "e4 ! {King pawn}") || contains(pgn, "{Sicilian branch}") ||
contains(pgn, "c5"))
{
scid_game_pgn_options_free(pgn_options);
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 1;
}
scid_game_pgn_options_free(pgn_options);
scid_game_cursor_free(cursor);
scid_position_free(position);
scid_game_free(game);
return 0;
}
3. Key Concepts and Patterns
Cursor Handover Pattern
Because scid_game_cursor instances are immutable, each navigational or structural modification produces a newly allocated cursor:
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;
}
This helper ensures clean ownership handover and avoids cursor leaks during successive move applications.
Adding Comments and NAGs
scid_game_cursor_comment_set(cursor, "Initiates the game");
scid_game_cursor_nag_add(cursor, 1); /* $1 = ! (good move) */
Mainline vs Variation Export
By default, scid_game_to_pgn exports the mainline along with all recursive sub-variations. You can configure scid_game_pgn_options to emit mainline-only text or format NAGs as symbolic punctuation (!, ?) rather than numeric notation ($1, $2):
scid_game_pgn_options* options = NULL;
scid_game_pgn_options_create(&options);
scid_game_pgn_options_use_symbols_set(options, 1);
scid_game_pgn_options_export_variations_set(options, 0); /* mainline only */
scid_game_to_pgn(game, options, buffer, sizeof(buffer), &size);
scid_game_pgn_options_free(options);