aboutsummaryrefslogtreecommitdiff
path: root/src/selfplay.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/selfplay.c')
-rw-r--r--src/selfplay.c170
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;
+}