static bool test_sar(struct board *b, char *arg) { enum stone color = str2stone(arg); arg += 2; coord_t *cc = str2coord(arg, board_size(b)); coord_t c = *cc; coord_done(cc); arg += strcspn(arg, " ") + 1; int eres = atoi(arg); board_print_test(2, b); if (DEBUGL(1)) printf("sar %s %s %d...\t", stone2str(color), coord2sstr(c, b), eres); assert(board_at(b, c) == S_NONE); int rres = is_bad_selfatari(b, color, c); if (rres == eres) { if (DEBUGL(1)) printf("OK\n"); } else { if (debug_level <= 2) { board_print_test(0, b); printf("sar %s %s %d...\t", stone2str(color), coord2sstr(c, b), eres); } printf("FAILED (%d)\n", rres); } return rres == eres; }
static bool test_useful_ladder(struct board *b, char *arg) { enum stone color = str2stone(arg); arg += 2; coord_t *cc = str2coord(arg, board_size(b)); coord_t c = *cc; coord_done(cc); arg += strcspn(arg, " ") + 1; int eres = atoi(arg); board_print_test(2, b); if (DEBUGL(1)) printf("useful_ladder %s %s %d...\t", stone2str(color), coord2sstr(c, b), eres); assert(board_at(b, c) == S_NONE); group_t atari_neighbor = board_get_atari_neighbor(b, c, color); assert(atari_neighbor); int ladder = is_ladder(b, c, atari_neighbor, true); assert(ladder); int rres = useful_ladder(b, atari_neighbor); if (rres == eres) { if (DEBUGL(1)) printf("OK\n"); } else { if (debug_level <= 2) { board_print_test(0, b); printf("useful_ladder %s %s %d...\t", stone2str(color), coord2sstr(c, b), eres); } printf("FAILED (%d)\n", rres); } return (rres == eres); }
static floating_t linear_permove(struct uct_dynkomi *d, struct board *b, struct tree *tree) { struct dynkomi_linear *l = d->data; enum stone color = d->uct->pondering ? tree->root_color : stone_other(tree->root_color); int lmoves = l->moves[color]; floating_t extra_komi; if (b->moves < lmoves) { floating_t base_komi = board_effective_handicap(b, l->handicap_value[color]); extra_komi = base_komi * (lmoves - b->moves) / lmoves; return extra_komi; } else { extra_komi = floor(tree->extra_komi); } /* Do not take decisions on unstable value. */ if (tree->root->u.playouts < GJ_MINGAMES) return extra_komi; floating_t my_value = tree_node_get_value(tree, 1, tree->root->u.value); /* We normalize komi as in komi_by_value(), > 0 when winning. */ extra_komi = komi_by_color(extra_komi, color); if (extra_komi < 0 && DEBUGL(3)) fprintf(stderr, "XXX: extra_komi %.3f < 0 (color %s tree ek %.3f)\n", extra_komi, stone2str(color), tree->extra_komi); // assert(extra_komi >= 0); floating_t orig_komi = extra_komi; if (my_value < 0.5 && l->komi_ratchet > 0 && l->komi_ratchet != INFINITY) { if (DEBUGL(0)) fprintf(stderr, "losing %f extra komi %.1f ratchet %.1f -> 0\n", my_value, extra_komi, l->komi_ratchet); /* Disable dynkomi completely, too dangerous in this game. */ extra_komi = l->komi_ratchet = 0; } else if (my_value < l->orange_zone && extra_komi > 0) { extra_komi = l->komi_ratchet = fmax(extra_komi - l->drop_step, 0.0); if (extra_komi != orig_komi && DEBUGL(3)) fprintf(stderr, "dropping to %f, extra komi %.1f -> ratchet %.1f\n", my_value, orig_komi, extra_komi); } else if (my_value > l->green_zone && extra_komi + 1 <= l->komi_ratchet) { extra_komi += 1; if (extra_komi != orig_komi && DEBUGL(3)) fprintf(stderr, "winning %f extra_komi %.1f -> %.1f, ratchet %.1f\n", my_value, orig_komi, extra_komi, l->komi_ratchet); } return komi_by_color(extra_komi, color); }
static bool test_moggy_moves(struct board *b, char *arg) { int runs = 1000; coord_t *cc = str2coord(arg, board_size(b)); struct move last; last.coord = *cc; coord_done(cc); last.color = board_at(b, last.coord); assert(last.color == S_BLACK || last.color == S_WHITE); enum stone color = stone_other(last.color); arg += strcspn(arg, " ") + 1; b->last_move = last; board_print(b, stderr); // Always print board so we see last move char e_arg[128]; sprintf(e_arg, "runs=%i", runs); struct engine *e = engine_replay_init(e_arg, b); if (DEBUGL(1)) printf("moggy moves %s, %s to play. Sampling moves (%i runs)...\n\n", coord2sstr(last.coord, b), stone2str(color), runs); int played_[b->size2 + 2]; memset(played_, 0, sizeof(played_)); int *played = played_ + 2; // allow storing pass/resign int most_played = 0; replay_sample_moves(e, b, color, played, &most_played); /* Show moves stats */ for (int k = most_played; k > 0; k--) for (coord_t c = resign; c < b->size2; c++) if (played[c] == k) printf("%3s: %.2f%%\n", coord2str(c, b), (float)k * 100 / runs); engine_done(e); return true; // Not much of a unit test right now =) }
int uct_playout(struct uct *u, struct board *b, enum stone player_color, struct tree *t) { struct board b2; board_copy(&b2, b); struct playout_amafmap amaf; amaf.gamelen = amaf.game_baselen = 0; /* Walk the tree until we find a leaf, then expand it and do * a random playout. */ struct tree_node *n = t->root; enum stone node_color = stone_other(player_color); assert(node_color == t->root_color); /* Make sure the root node is expanded. */ if (tree_leaf_node(n) && !__sync_lock_test_and_set(&n->is_expanded, 1)) tree_expand_node(t, n, &b2, player_color, u, 1); /* Tree descent history. */ /* XXX: This is somewhat messy since @n and descent[dlen-1].node are * redundant. */ struct uct_descent descent[DESCENT_DLEN]; descent[0].node = n; descent[0].lnode = NULL; int dlen = 1; /* Total value of the sequence. */ struct move_stats seq_value = { .playouts = 0 }; /* The last "significant" node along the descent (i.e. node * with higher than configured number of playouts). For black * and white. */ struct tree_node *significant[2] = { NULL, NULL }; if (n->u.playouts >= u->significant_threshold) significant[node_color - 1] = n; int result; int pass_limit = (board_size(&b2) - 2) * (board_size(&b2) - 2) / 2; int passes = is_pass(b->last_move.coord) && b->moves > 0; /* debug */ static char spaces[] = "\0 "; /* /debug */ if (UDEBUGL(8)) fprintf(stderr, "--- UCT walk with color %d\n", player_color); while (!tree_leaf_node(n) && passes < 2) { spaces[dlen - 1] = ' '; spaces[dlen] = 0; /*** Choose a node to descend to: */ /* Parity is chosen already according to the child color, since * it is applied to children. */ node_color = stone_other(node_color); int parity = (node_color == player_color ? 1 : -1); assert(dlen < DESCENT_DLEN); descent[dlen] = descent[dlen - 1]; if (u->local_tree && (!descent[dlen].lnode || descent[dlen].node->d >= u->tenuki_d)) { /* Start new local sequence. */ /* Remember that node_color already holds color of the * to-be-found child. */ descent[dlen].lnode = node_color == S_BLACK ? t->ltree_black : t->ltree_white; } if (!u->random_policy_chance || fast_random(u->random_policy_chance)) u->policy->descend(u->policy, t, &descent[dlen], parity, b2.moves > pass_limit); else u->random_policy->descend(u->random_policy, t, &descent[dlen], parity, b2.moves > pass_limit); /*** Perform the descent: */ if (descent[dlen].node->u.playouts >= u->significant_threshold) { significant[node_color - 1] = descent[dlen].node; } seq_value.playouts += descent[dlen].value.playouts; seq_value.value += descent[dlen].value.value * descent[dlen].value.playouts; n = descent[dlen++].node; assert(n == t->root || n->parent); if (UDEBUGL(7)) fprintf(stderr, "%s+-- UCT sent us to [%s:%d] %d,%f\n", spaces, coord2sstr(node_coord(n), t->board), node_coord(n), n->u.playouts, tree_node_get_value(t, parity, n->u.value)); /* Add virtual loss if we need to; this is used to discourage * other threads from visiting this node in case of multiple * threads doing the tree search. */ if (u->virtual_loss) stats_add_result(&n->u, node_color == S_BLACK ? 0.0 : 1.0, u->virtual_loss); assert(node_coord(n) >= -1); record_amaf_move(&amaf, node_coord(n)); struct move m = { node_coord(n), node_color }; int res = board_play(&b2, &m); if (res < 0 || (!is_pass(m.coord) && !group_at(&b2, m.coord)) /* suicide */ || b2.superko_violation) { if (UDEBUGL(4)) { for (struct tree_node *ni = n; ni; ni = ni->parent) fprintf(stderr, "%s<%"PRIhash"> ", coord2sstr(node_coord(ni), t->board), ni->hash); fprintf(stderr, "marking invalid %s node %d,%d res %d group %d spk %d\n", stone2str(node_color), coord_x(node_coord(n),b), coord_y(node_coord(n),b), res, group_at(&b2, m.coord), b2.superko_violation); } n->hints |= TREE_HINT_INVALID; result = 0; goto end; } if (is_pass(node_coord(n))) passes++; else passes = 0; enum stone next_color = stone_other(node_color); /* We need to make sure only one thread expands the node. If * we are unlucky enough for two threads to meet in the same * node, the latter one will simply do another simulation from * the node itself, no big deal. t->nodes_size may exceed * the maximum in multi-threaded case but not by much so it's ok. * The size test must be before the test&set not after, to allow * expansion of the node later if enough nodes have been freed. */ if (tree_leaf_node(n) && n->u.playouts - u->virtual_loss >= u->expand_p && t->nodes_size < u->max_tree_size && !__sync_lock_test_and_set(&n->is_expanded, 1)) tree_expand_node(t, n, &b2, next_color, u, -parity); } amaf.game_baselen = amaf.gamelen; if (t->use_extra_komi && u->dynkomi->persim) { b2.komi += round(u->dynkomi->persim(u->dynkomi, &b2, t, n)); } if (passes >= 2) { /* XXX: No dead groups support. */ floating_t score = board_official_score(&b2, NULL); /* Result from black's perspective (no matter who * the player; black's perspective is always * what the tree stores. */ result = - (score * 2); if (UDEBUGL(5)) fprintf(stderr, "[%d..%d] %s p-p scoring playout result %d (W %f)\n", player_color, node_color, coord2sstr(node_coord(n), t->board), result, score); if (UDEBUGL(6)) board_print(&b2, stderr); board_ownermap_fill(&u->ownermap, &b2); } else { // assert(tree_leaf_node(n)); /* In case of parallel tree search, the assertion might * not hold if two threads chew on the same node. */ result = uct_leaf_node(u, &b2, player_color, &amaf, descent, &dlen, significant, t, n, node_color, spaces); } if (u->policy->wants_amaf && u->playout_amaf_cutoff) { unsigned int cutoff = amaf.game_baselen; cutoff += (amaf.gamelen - amaf.game_baselen) * u->playout_amaf_cutoff / 100; amaf.gamelen = cutoff; } /* Record the result. */ assert(n == t->root || n->parent); floating_t rval = scale_value(u, b, result); u->policy->update(u->policy, t, n, node_color, player_color, &amaf, &b2, rval); if (t->use_extra_komi) { stats_add_result(&u->dynkomi->score, result / 2, 1); stats_add_result(&u->dynkomi->value, rval, 1); } if (u->local_tree && n->parent && !is_pass(node_coord(n)) && dlen > 0) { /* Get the local sequences and record them in ltree. */ /* We will look for sequence starts in our descent * history, then run record_local_sequence() for each * found sequence start; record_local_sequence() may * pick longer sequences from descent history then, * which is expected as it will create new lnodes. */ enum stone seq_color = player_color; /* First move always starts a sequence. */ record_local_sequence(u, t, &b2, descent, dlen, 1, seq_color); seq_color = stone_other(seq_color); for (int dseqi = 2; dseqi < dlen; dseqi++, seq_color = stone_other(seq_color)) { if (u->local_tree_allseq) { /* We are configured to record all subsequences. */ record_local_sequence(u, t, &b2, descent, dlen, dseqi, seq_color); continue; } if (descent[dseqi].node->d >= u->tenuki_d) { /* Tenuki! Record the fresh sequence. */ record_local_sequence(u, t, &b2, descent, dlen, dseqi, seq_color); continue; } if (descent[dseqi].lnode && !descent[dseqi].lnode) { /* Record result for in-descent picked sequence. */ record_local_sequence(u, t, &b2, descent, dlen, dseqi, seq_color); continue; } } } end: /* We need to undo the virtual loss we added during descend. */ if (u->virtual_loss) { floating_t loss = node_color == S_BLACK ? 0.0 : 1.0; for (; n->parent; n = n->parent) { stats_rm_result(&n->u, loss, u->virtual_loss); loss = 1.0 - loss; } } board_done_noalloc(&b2); return result; } int uct_playouts(struct uct *u, struct board *b, enum stone color, struct tree *t, struct time_info *ti) { int i; if (ti && ti->dim == TD_GAMES) { for (i = 0; t->root->u.playouts <= ti->len.games && !uct_halt; i++) uct_playout(u, b, color, t); } else { for (i = 0; !uct_halt; i++) uct_playout(u, b, color, t); } return i; }
static void record_local_sequence(struct uct *u, struct tree *t, struct board *endb, struct uct_descent *descent, int dlen, int di, enum stone seq_color) { #define LTREE_DEBUG if (UDEBUGL(6)) /* Ignore pass sequences. */ if (is_pass(node_coord(descent[di].node))) return; LTREE_DEBUG board_print(endb, stderr); LTREE_DEBUG fprintf(stderr, "recording local %s sequence: ", stone2str(seq_color)); /* Sequences starting deeper are less relevant in general. */ int pval = LTREE_PLAYOUTS_MULTIPLIER; if (u->local_tree && u->local_tree_depth_decay > 0) pval = ((floating_t) pval) / pow(u->local_tree_depth_decay, di - 1); if (!pval) { LTREE_DEBUG fprintf(stderr, "too deep @%d\n", di); return; } /* Pick the right local tree root... */ struct tree_node *lnode = seq_color == S_BLACK ? t->ltree_black : t->ltree_white; lnode->u.playouts++; double sval = 0.5; if (u->local_tree_rootgoal) { sval = local_value(u, endb, node_coord(descent[di].node), seq_color); LTREE_DEBUG fprintf(stderr, "(goal %s[%s %1.3f][%d]) ", coord2sstr(node_coord(descent[di].node), t->board), stone2str(seq_color), sval, descent[di].node->d); } /* ...and record the sequence. */ int di0 = di; while (di < dlen && (di == di0 || descent[di].node->d < u->tenuki_d)) { enum stone color = (di - di0) % 2 ? stone_other(seq_color) : seq_color; double rval; if (u->local_tree_rootgoal) rval = sval; else rval = local_value(u, endb, node_coord(descent[di].node), color); LTREE_DEBUG fprintf(stderr, "%s[%s %1.3f][%d] ", coord2sstr(node_coord(descent[di].node), t->board), stone2str(color), rval, descent[di].node->d); lnode = tree_get_node(t, lnode, node_coord(descent[di++].node), true); assert(lnode); stats_add_result(&lnode->u, rval, pval); } /* Add lnode for tenuki (pass) if we descended further. */ if (di < dlen) { double rval = u->local_tree_rootgoal ? sval : 0.5; LTREE_DEBUG fprintf(stderr, "pass "); lnode = tree_get_node(t, lnode, pass, true); assert(lnode); stats_add_result(&lnode->u, rval, pval); } LTREE_DEBUG fprintf(stderr, "\n"); }
/* Syntax: * moggy status (last_move) coord [coord...] * Play number of random games starting from last_move * * moggy status coord [coord...] * moggy status (b) coord [coord...] * Black to play, pick random white last move * * moggy status (w) coord [coord...] * White to play, pick random black last move */ static bool test_moggy_status(struct board *board, char *arg) { int games = 4000; coord_t status_at[10]; int n = 0; enum stone color = S_BLACK; int pick_random = true; // Pick random last move for each game while (*arg && *arg != '#') { if (*arg == ' ' || *arg == '\t') { arg++; continue; } if (!strncmp(arg, "(b)", 3)) color = S_BLACK; else if (!strncmp(arg, "(w)", 3)) color = S_WHITE; else if (*arg == '(') { /* Optional "(last_move)" argument */ arg++; assert(isalpha(*arg)); pick_random = false; struct move last; last.coord = str2scoord(arg, board_size(board)); last.color = board_at(board, last.coord); assert(last.color == S_BLACK || last.color == S_WHITE); color = stone_other(last.color); board->last_move = last; } else { assert(isalpha(*arg)); status_at[n++] = str2scoord(arg, board_size(board)); } arg += strcspn(arg, " \t"); } board_print(board, stderr); if (DEBUGL(1)) { printf("moggy status "); for (int i = 0; i < n; i++) printf("%s%s", coord2sstr(status_at[i], board), (i != n-1 ? " " : "")); printf(", %s to play. Playing %i games %s...\n", stone2str(color), games, (pick_random ? "(random last move) " : "")); } struct playout_policy *policy = playout_moggy_init(NULL, board, NULL); struct playout_setup setup = { .gamelen = MAX_GAMELEN }; struct board_ownermap ownermap; ownermap.playouts = 0; ownermap.map = malloc2(board_size2(board) * sizeof(ownermap.map[0])); memset(ownermap.map, 0, board_size2(board) * sizeof(ownermap.map[0])); /* Get final status estimate after a number of moggy games */ int wr = 0; double time_start = time_now(); for (int i = 0; i < games; i++) { struct board b; board_copy(&b, board); if (pick_random) pick_random_last_move(&b, color); int score = play_random_game(&setup, &b, color, NULL, &ownermap, policy); if (color == S_WHITE) score = -score; wr += (score > 0); board_done_noalloc(&b); } double elapsed = time_now() - time_start; printf("moggy status in %.1fs, %i games/s\n\n", elapsed, (int)((float)games / elapsed)); int wr_black = wr * 100 / games; int wr_white = (games - wr) * 100 / games; if (wr_black > wr_white) printf("Winrate: [ black %i%% ] white %i%%\n\n", wr_black, wr_white); else printf("Winrate: black %i%% [ white %i%% ]\n\n", wr_black, wr_white); board_print_ownermap(board, stderr, &ownermap); for (int i = 0; i < n; i++) { coord_t c = status_at[i]; enum stone color = (ownermap.map[c][S_BLACK] > ownermap.map[c][S_WHITE] ? S_BLACK : S_WHITE); fprintf(stderr, "%3s owned by %s: %i%%\n", coord2sstr(c, board), stone2str(color), ownermap.map[c][color] * 100 / ownermap.playouts); } free(ownermap.map); playout_policy_done(policy); return true; // Not much of a unit test right now =) }
static void board_load(struct board *b, FILE *f, unsigned int size) { board_printed = false; board_resize(b, size); board_clear(b); for (int y = size - 1; y >= 0; y--) { char line[256]; if (!fgets(line, sizeof(line), f)) { fprintf(stderr, "Premature EOF.\n"); exit(EXIT_FAILURE); } line[strlen(line) - 1] = 0; // chomp if (strlen(line) != size * 2 - 1) { fprintf(stderr, "Line not %d char long: %s\n", size * 2 - 1, line); exit(EXIT_FAILURE); } for (unsigned int i = 0; i < size * 2; i++) { enum stone s; switch (line[i]) { case '.': s = S_NONE; break; case 'X': s = S_BLACK; break; case 'O': s = S_WHITE; break; default: fprintf(stderr, "Invalid stone '%c'\n", line[i]); exit(EXIT_FAILURE); } i++; if (line[i] != ' ' && i/2 < size - 1) { fprintf(stderr, "No space after stone %i: '%c'\n", i/2 + 1, line[i]); exit(EXIT_FAILURE); } if (s == S_NONE) continue; struct move m = { .color = s, .coord = coord_xy(b, i/2 + 1, y + 1) }; if (board_play(b, &m) < 0) { fprintf(stderr, "Failed to play %s %s\n", stone2str(s), coord2sstr(m.coord, b)); board_print(b, stderr); exit(EXIT_FAILURE); } } } int suicides = b->captures[S_BLACK] || b->captures[S_WHITE]; assert(!suicides); } static void set_ko(struct board *b, char *arg) { assert(isalpha(*arg)); struct move last; last.coord = str2scoord(arg, board_size(b)); last.color = board_at(b, last.coord); assert(last.color == S_BLACK || last.color == S_WHITE); b->last_move = last; /* Sanity checks */ group_t g = group_at(b, last.coord); assert(board_group_info(b, g).libs == 1); assert(group_stone_count(b, g, 2) == 1); coord_t lib = board_group_info(b, g).lib[0]; assert(board_is_eyelike(b, lib, last.color)); b->ko.coord = lib; b->ko.color = stone_other(last.color); }