diff options
| author | tslil <tslil@posteo.de> | 2026-08-30 19:46:07 +0100 |
|---|---|---|
| committer | tslil <tslil@posteo.de> | 2026-09-02 12:13:01 +0100 |
| commit | 241156da6f35cb888253e6b8814e7b61a76b4fc7 (patch) | |
| tree | e562029445b1473cc89590793f32c561540d621c /src/selfplay.c | |
| parent | 696c3808220ea9d8c604939894a2d509f05cc4c6 (diff) | |
TD(lambda), and a big rework
NOTE: 6x6 weights are as-yet untrained
On the performance side:
* switch from linked-list chaining hash table to direct mapping
* cache check_win by hash
* persist transposition tables across plies (this was a big deal and difficult to puzzle out)
* add align directives to the network floats
* prepare_nn_input gained a small LUT to avoid branching (i suppose you might call this 1D embedding)
* apply mtune=native for the native builds
On the correctness side:
* Zobrist has now includes the ply parity
* the network now predicts something in [-1, 1] using tanh (approximated by polynomials)
* the training object is now MSE because we're learning a value estimate
* use .clangd instead of compile_commands.json
On the training side:
* completely forgo human-derived data set
* implement td(lambda), self-play, tournaments
Diffstat (limited to 'src/selfplay.c')
| -rw-r--r-- | src/selfplay.c | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/src/selfplay.c b/src/selfplay.c new file mode 100644 index 0000000..6da4f6e --- /dev/null +++ b/src/selfplay.c @@ -0,0 +1,170 @@ +/* + 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 <https://www.gnu.org/licenses/>. +*/ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#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 <size> <eps> <depth> <min_ply>\n\ +Plays a single game of <size>x<size>. 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 <min_ply> onwards is written to stdout as one CSV row:\n\ +\n\ + parity,white_flats,black_flats,<board values...>,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; +} |
