/* 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 ct. If not, see . */ #include #include #include #include #include #include "actions.h" #include "negamax.h" #include "nn.h" #include "tak.h" #include "xorshift64.h" #define MAX_PLY 256 #define MAX_INP (3 + 6 * 6) static tak_state_p state; static uint32_t min_ply; // Positions buffered for the game (the net input only) static float rows[MAX_PLY][MAX_INP]; static uint32_t row_ply[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 int play_game(const uint32_t eps10000) { reset_state(state, state->board_size); n_rows = 0; char ptn[16]; for (uint32_t ply = 0; ply < MAX_PLY; ply++) { // The engine's best move for this position (sets negamax_ptn) negamax_generate(state); if (rand32() % 10000 < eps10000) { // Epsilon: uniform random legal move uint32_t n_actions; action_t *actions = actions_generate(state, &n_actions); action_to_ptn(actions[rand32() % n_actions], ptn); } else { // Greedy: the engine's best move strcpy(ptn, negamax_ptn); } // Buffer this position, before the move changes the board if (ply >= min_ply && n_rows < MAX_PLY) { nn_prepare_input(rows[n_rows], state); row_ply[n_rows] = ply; 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 from black's perspective float R; switch (state->won) { case WIN_FLAT_BLACK: case WIN_ROAD_BLACK: R = 1.0f; break; case WIN_FLAT_WHITE: case WIN_ROAD_WHITE: R = -1.0f; break; default: R = 0.0f; break; } for (uint32_t k = 0; k < n_rows; k++) { fprintf(stdout, "%d,%.8f,%.8f,", (int)rows[k][0], rows[k][1], rows[k][2]); for (uint8_t c = 0; c < n_cells; c++) fprintf(stdout, "%.2f,", rows[k][3 + c]); fprintf(stdout, "%u,%u,%.0f\n", game_id, row_ply[k], R); } return (int)n_rows; } const char *license = "selfplay, one game of self-play training data for the ct net\n\ Usage: selfplay \n\ Plays a single game of x. On each ply the engine's best move is played with probability 1-eps, and a uniform random legal move with probability eps. Every position from ply onwards is written to stdout as one CSV row:\n\ \n\ parity,white_flats,black_flats,,game_id,ply,R\n\ \n\ The first 3 + size*size columns are the training input, game_id identifies the game, ply is the move number the position precedes, and R is the game's final outcome from black's perspective (+1 black won, -1 white won, 0 draw). nn_train.py runs many of these in parallel, pools the rows, and derives TD(lambda) targets from them.\n\ \n\ Copyright (C) 2026, tslil clingman\n\ \n\ This program is free software: you can redistribute it and/or modify\n \ it under the terms of the GNU General Public License as published by\n \ the Free Software Foundation, either version 3 of the License, or\n\ (at your option) any later version."; int main(int argc, char **argv) { if (argc != 5) { puts(license); 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 (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; }