aboutsummaryrefslogtreecommitdiff
path: root/src/match.c
diff options
context:
space:
mode:
authortslil <tslil@posteo.de>2026-08-30 19:46:07 +0100
committertslil <tslil@posteo.de>2026-09-02 12:13:01 +0100
commit241156da6f35cb888253e6b8814e7b61a76b4fc7 (patch)
treee562029445b1473cc89590793f32c561540d621c /src/match.c
parent696c3808220ea9d8c604939894a2d509f05cc4c6 (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/match.c')
-rw-r--r--src/match.c196
1 files changed, 196 insertions, 0 deletions
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 <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"
+
+// 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 <size> <eps> <depth>\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\
+ <current|candidate|draw> <plies>\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;
+}