From 241156da6f35cb888253e6b8814e7b61a76b4fc7 Mon Sep 17 00:00:00 2001 From: tslil Date: Sun, 30 Aug 2026 19:46:07 +0100 Subject: 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 --- src/match.c | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 src/match.c (limited to 'src/match.c') diff --git a/src/match.c b/src/match.c new file mode 100644 index 0000000..1c706cd --- /dev/null +++ b/src/match.c @@ -0,0 +1,196 @@ +/* + match, one game of candidate weights against the candidate weights + + 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" + +// a little gymnastics to insert candidate neural networks +#include "weights_5.h" +#include "weights_6.h" +#include "cand_weights_5.c" +#include "cand_weights_6.c" + +static inline float five_cand(tak_state_p state) { + static float cur_board[FIVE_INP_NUM] NN_ALIGNED; + + nn_prepare_input(cur_board, state); + return eval_net(cur_board, FIVE_INP_NUM, &cand_five_dense_weights[0][0], + cand_five_dense_biases, cand_five_logit_weights, cand_five_logit_bias, + FIVE_DENSE_NUM); +} + +static inline float six_cand(tak_state_p state) { + static float cur_board[SIX_INP_NUM] NN_ALIGNED; + + nn_prepare_input(cur_board, state); + return eval_net(cur_board, SIX_INP_NUM, &cand_six_dense_weights[0][0], + cand_six_dense_biases, cand_six_logit_weights, cand_six_logit_bias, + SIX_DENSE_NUM); +} + + +#define MAX_PLY 250 + +static tak_state_p state; + +// =================================================================== +// negamax progress hook (silent) +// =================================================================== + +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); +} + +// =================================================================== +// Game driver +// =================================================================== + +// Returns the winner's colour, or -1 for a draw / abort +static int play_game(const nn_function current_fn, const nn_function cand_fn, + const enum COLOUR current_fn_colour, const uint32_t eps10000) { + reset_state(state, state->board_size); + char ptn[16]; + + for (uint32_t ply = 0; ply < MAX_PLY; ply++) { + // Whose evaluator is it? + const nn_function fn = + (state->current_colour == current_fn_colour) ? current_fn : cand_fn; + negamax_set_evaluator(fn); + + 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 side's best move + negamax_generate(state); + strcpy(ptn, negamax_ptn); + } + + const enum ACT_RESULT r = do_ptn(state, ptn); + if (r == GAME_END) + break; + if (r != ACT_OK) { + fprintf(stderr, "match: unexpected result %d for ptn %s\n", r, ptn); + return -2; + } + } + + switch (state->won) { + case WIN_DRAW: + return -1; + case WIN_FLAT_BLACK: + case WIN_ROAD_BLACK: + return C_BLACK; + case WIN_FLAT_WHITE: + case WIN_ROAD_WHITE: + return C_WHITE; + default: + fprintf(stderr, "match: game did not end cleanly (won=%d)\n", state->won); + return -2; + } +} + +// =================================================================== +// Main +// =================================================================== + +const char *license = + "match, one Tak game of the current weights against the candidate.\n\ +\n \ +usage: match \n\ +\n\ +This binary is linked with both the include/weight_* files and the include/cand_weight_* files. It plays a single game, with a random side choice and both sides using epsilon-greedy move noise. The result format is:\n\ +\n\ + \n\ +\n\ +nn_train.py runs many of these in parallel to score the candidate against the current weights.\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 != 4) { + 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]); + + if (size != 5 && size != 6) { + fprintf(stderr, "match: size must be 5 or 6\n"); + return EXIT_FAILURE; + } + if (eps10000 >= 10000) { + fprintf(stderr, "match: eps must be < 1\n"); + return EXIT_FAILURE; + } + + // Processes launched in the same second must not share a seed + xors = ((uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32)) * + 0x9E3779B97F4A7C15ULL + + 0xDEADBEEF; + + state = new_tak_state(size); + negamax_init(size); + + const nn_function current_fn = + (size == 5) ? &five_nn_fn : &six_nn_fn; + const nn_function cand_fn = + (size == 5) ? &five_cand + : &six_cand; + + const enum COLOUR current_fn_colour = rand32() % 2 ? C_WHITE : C_BLACK; + const int winner = play_game(current_fn, cand_fn, current_fn_colour, eps10000); + + const char *name; + if (winner == -2) + name = "abort"; + else if (winner == -1) + name = "draw"; + else if (winner == (int)current_fn_colour) + name = "current"; + else + name = "candidate"; + printf("%s %u\n", name, state->ply); + + negamax_free(); + free_tak_state(state); + + return EXIT_SUCCESS; +} -- cgit v1.3.1