/* 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 "nn.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 256 #define MAX_CELLS (6 * 6) static tak_state_p state; static uint32_t min_ply; 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 n_rows; static uint32_t game_id; // PID, tags every row so games can be grouped 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 uint32_t eps10000) { reset_state(state, state->board_size); n_rows = 0; const enum COLOUR eps_side = (rand32() & 1) ? C_BLACK : C_WHITE; char ptn[16]; for (uint32_t ply = 0; ply < MAX_PLY; ply++) { negamax_generate(state); if (state->current_colour == eps_side && rand32() % 10000 < eps10000) { uint32_t n_actions; action_t *actions = actions_generate(state, &n_actions); action_to_ptn(actions[rand32() % n_actions], ptn); } else { strcpy(ptn, negamax_ptn); } const pv_leaf_t *leaf = pv_get_leaf(); if (leaf->current_colour != eps_side && ply >= min_ply && n_rows < MAX_PLY) { 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; 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", game_id, 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\ Usage: selfplay \n\ Plays a single asymmetric game of x self-play: one side plays the engine's best move at every ply (pure minimax) and the other side plays the engine's best move except that with probability eps it plays a uniform random legal move; the colour assignment of these two players is random. Only the deterministic side's plies are written to stdout, one CSV row per recorded search, from ply onwards:\n\ \n\ parity,white_flats,black_flats,,game_id,R,terminal,leaf_R\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 != 5) { puts(usage); return EXIT_FAILURE; } const uint8_t size = (uint8_t)atoi(argv[1]); const uint32_t eps10000 = (uint32_t)(atof(argv[2]) * 10000.0 + 0.5); negamax_search_depth = (uint8_t)atoi(argv[3]); min_ply = (uint32_t)atoi(argv[4]); 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 (eps10000 >= 10000) { fprintf(stderr, "selfplay: eps must be < 1\n"); return EXIT_FAILURE; } // Processes launched in the same second must not share a seed game_id = (uint32_t)getpid(); xors = ((uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32)) * 0x9E3779B97F4A7C15ULL + 0xC0FFEEEE; state = new_tak_state(size); negamax_init(size); play_game(eps10000); negamax_free(); free_tak_state(state); return EXIT_SUCCESS; }