/*
selfplay: generate training data
Copyright (C) 2026, tslil clingman
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with ctak. If not, see .
*/
#include
#include
#include
#include
#include
#include "actions.h"
#include "negamax.h"
#include "pv.h"
#include "tak.h"
#include "xorshift64.h"
#ifndef RECORD_PV
#error "selfplay must be built with -DRECORD_PV (see the selfplay Makefile target)"
#endif
#define MAX_PLY 255
#define MAX_CELLS (6 * 6)
static tak_state_p state;
static float row_head[MAX_PLY][3];
static uint8_t row_cells[MAX_PLY][MAX_CELLS];
static uint8_t row_term[MAX_PLY];
static float row_terminal_value[MAX_PLY];
static uint32_t row_game_id[MAX_PLY];
static uint32_t n_rows;
static uint32_t game_id_black;
static uint32_t game_id_white;
void negamax_display_progress(const uint8_t cur_depth,
const uint8_t init_depth,
const uint32_t length) {
(void)(cur_depth);
(void)(init_depth);
(void)(length);
}
static float win_value(const enum WIN_TYPE w) {
switch (w) {
case WIN_FLAT_BLACK:
case WIN_ROAD_BLACK:
return 1.0f;
case WIN_FLAT_WHITE:
case WIN_ROAD_WHITE:
return -1.0f;
default:
return 0.0f;
}
}
static int play_game(const uint8_t min_ply) {
reset_state(state, state->board_size);
n_rows = 0;
char ptn[16];
for (uint32_t ply = 0; ply < MAX_PLY; ply++) {
if (ply < min_ply) {
uint32_t n_actions;
action_t *actions = actions_generate(state, &n_actions);
action_to_ptn(actions[rand32() % n_actions], ptn);
} else {
negamax_generate(state);
strcpy(ptn, negamax_ptn);
const pv_leaf_t *leaf = pv_get_leaf();
memcpy(row_head[n_rows], leaf->head, sizeof(row_head[n_rows]));
memcpy(row_cells[n_rows], leaf->cells, sizeof(row_cells[n_rows]));
row_term[n_rows] = leaf->terminal;
row_terminal_value[n_rows] = leaf->terminal_value;
row_game_id[n_rows] = state->current_colour == C_BLACK ? game_id_black : game_id_white;
n_rows++;
}
const enum ACT_RESULT r = do_ptn(state, ptn);
if (r == GAME_END)
break;
if (r != ACT_OK) {
fprintf(stderr, "selfplay: unexpected result %d for ptn %s\n", r,
ptn);
return 0;
}
}
if (n_rows == 0)
return 0;
const uint8_t n_cells = state->board_size * state->board_size;
// final outcome of the played game, from black's perspective
const float R = win_value(state->won);
for (uint32_t k = 0; k < n_rows; k++) {
fprintf(stdout, "%.8f,%.8f,%.8f,", row_head[k][0], row_head[k][1],
row_head[k][2]);
for (uint8_t c = 0; c < n_cells; c++)
fprintf(stdout, "%u,", (unsigned)row_cells[k][c]);
fprintf(stdout, "%u,%.0f,%u,%.0f\n", row_game_id[k], R,
(unsigned)row_term[k], row_terminal_value[k]);
}
return (int)n_rows;
}
const char *usage =
"selfplay, one game of asymmetric self-play training data.\n\n\
Usage: selfplay \n\n \
Plays a single game of x self-play, with moves made randomly. Only the deterministic plies are written to stdout, one CSV row per recorded search, from ply onwards:\n\n \
\tparity,white_flats,black_flats,| ,game_id,R,terminal,leaf_R\n\n \
nn_train.py runs many of these in parallel to generate training data.\n\n \
Copyright (C) 2026, tslil clingman\n\n \
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.";
int main(int argc, char **argv) {
if (argc != 4) {
puts(usage);
return EXIT_FAILURE;
}
const uint8_t size = (uint8_t)atoi(argv[1]);
negamax_search_depth = (uint8_t)atoi(argv[2]);
const uint8_t min_ply = (uint8_t)atoi(argv[3]);
if (size != 5 && size != 6) {
fprintf(stderr, "selfplay: size must be 5 or 6\n");
return EXIT_FAILURE;
}
if (negamax_search_depth > NEGAMAX_MAX_DEPTH) {
fprintf(stderr, "selfplay: depth must be <= %d\n", NEGAMAX_MAX_DEPTH);
return EXIT_FAILURE;
}
if (min_ply >= MAX_PLY) {
fprintf(stderr, "selfplay: min_ply must be < %d\n", MAX_PLY);
return EXIT_FAILURE;
}
// Processes launched in the same second must not share a seed
xors = ((uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32)) *
0x9E3779B97F4A7C15ULL +
0xC0FFEE;
game_id_black = rand32();
game_id_white = rand32();
state = new_tak_state(size);
negamax_init(size);
play_game(min_ply);
negamax_free();
free_tak_state(state);
return EXIT_SUCCESS;
}
|