Dan wrote: > Another thing is that there is something called the ``killer move'' > in chess programming. The idea is that a move that is wins frequently > in different branches of the move tree may be a killer move that > clinches the win. We would look for vital points by some award system > that tries to find such moves. I've heard about a tsumego reader which only used success/failure statistics to determine move order, no go knowledge at all, reportedly with good results. I actually once tried to include something along these line in the tactical reading, but without much success. It may be worth investigating again. > There's different ways of expressing this, but some bookkeeping > of the win rate various moves might be helpful. It's complicated by > the hashing of course. On the other hand its data that could be > kept harmlessly from move to move, unlike the risky caching of > owl reading we currently do. I'm not entirely happy with this caching scheme. Except that it's risky (which I guess we have to accept) I see the following problems: * Only owl attack, defense, and threats are cached. Much time is also spent on owl_does_defend(), owl_connection_defends(), and even owl_substantial() which is called from atari_atari(). * The owl invalidation scheme is hard to maintain. It's not even working properly everywhere yet, e.g. when running the scoring code. * The owl invalidation scheme is inflexible and hard to fine tune. The heart of the problem is that we're in fact not caching the owl results themselves, but rather dragon data. This makes everything unnecessarily complicated, especially since the partitioning into dragons may vary when things connect or get cut or captured. Instead I propose that we implement a true owl cache, maintained from the owl code. This would store owl routine name, input parameters, and resulting data together with a copy of the board position when it was called. In order to make the cached data useful over multiple moves we gray out (literally!) the areas of the board which we don't think will have any effect on this owl reading. This leaves a lot of flexibility when it comes to deciding how conservative we want to be. It also automatically solves the problem of invalidating the cache when a new position is loaded (e.g. by loadsgf) because the stored boards automatically will become incompatible with the new board position. A draft implementation is appended as a patch to 2.7.232. To decide the area of the board we care about, we do the following 1. Start with the goal array (essentially the investigated dragon). 2. Continue with a distance four expansion through empty intersections. 3. Now add adjacent opponent strings, and 4. the liberties of adjacent opponent strings with less than five liberties. This can of course be modified a lot, and possibly be dependent on playing level. A good idea in the future might be to use the maximum extent of the goal array during the reading as a basis for the active area. /Gunnar Index: gnugo/engine/dragon.c diff -u gnugo/engine/dragon.c:1.1.1.14 gnugo/engine/dragon.c:1.11 --- gnugo/engine/dragon.c:1.1.1.14 Thu Apr 26 08:20:56 2001 +++ gnugo/engine/dragon.c Thu Apr 26 21:10:41 2001 @@ -602,6 +602,8 @@ /* Determine owl status of each dragon. */ + purge_persistent_owl_cache(); + /* Do not reuse data unless two valid last moves are found. * Particularly if the last move captured the stone played * previously or if either player passed we do not want to Index: gnugo/engine/liberty.h diff -u gnugo/engine/liberty.h:1.1.1.12 gnugo/engine/liberty.h:1.16 --- gnugo/engine/liberty.h:1.1.1.12 Thu Apr 26 08:20:58 2001 +++ gnugo/engine/liberty.h Thu Apr 26 22:38:52 2001 @@ -296,13 +296,14 @@ int owl_attack(int m, int n, int *i, int *j); int owl_defend(int m, int n, int *i, int *j); -int owl_threaten_attack(int, int, int *, int *, int *, int *); -int owl_threaten_defense(int, int, int *, int *, int *, int *); +int owl_threaten_attack(int m, int n, int *ui, int *uj, int *vi, int *vj); +int owl_threaten_defense(int m, int n, int *ui, int *uj, int *vi, int *vj); int owl_does_defend(int ti, int tj, int m, int n); int owl_does_attack(int ti, int tj, int m, int n); int owl_connection_defends(int ti, int tj, int ai, int aj, int bi, int bj); -int owl_substantial(int, int); +int owl_substantial(int i, int j); void owl_analyze_semeai(int ai, int aj, int bi, int bj); +void purge_persistent_owl_cache(void); void change_defense(int, int, int, int, int); void change_attack(int, int, int, int, int); Index: gnugo/engine/owl.c diff -u gnugo/engine/owl.c:1.1.1.19 gnugo/engine/owl.c:1.20 --- gnugo/engine/owl.c:1.1.1.19 Thu Apr 26 08:20:56 2001 +++ gnugo/engine/owl.c Thu Apr 26 21:10:41 2001 @@ -104,6 +104,47 @@ }; +/* Persistent owl result cache to reuse owl results between moves. */ +struct owl_cache { + char board[MAX_BOARD][MAX_BOARD]; + int movenum; + int tactical_nodes; + int routine; + int ai, aj; /* first input coordinate */ + int bi, bj; /* second input coordinate */ + int ci, cj; /* third input coordinate */ + int result; + int ui, uj; /* first result coordinate */ + int vi, vj; /* second result coordinate */ +}; + +#define MAX_OWL_CACHE_SIZE 80 +static struct owl_cache persistent_owl_cache[MAX_OWL_CACHE_SIZE]; +static int persistent_owl_cache_size = 0; + +#define OWL_THREATEN_ATTACK 0 +#define OWL_THREATEN_DEFENSE 1 +#define OWL_DOES_DEFEND 2 +#define OWL_DOES_ATTACK 3 +#define OWL_CONNECTION_DEFENDS 4 +#define OWL_SUBSTANTIAL 5 +/* The following two are defined in cache.h */ +/* #define OWL_ATTACK 8 */ +/* #define OWL_DEFEND 9 */ + +static int verify_stored_board(char board[MAX_BOARD][MAX_BOARD]); +static int search_persistent_owl_cache(int routine, int ai, int aj, + int bi, int bj, int ci, int cj, + int *result, int *ui, int *uj, + int *vi, int *vj); +static void store_persistent_owl_cache(int routine, int ai, int aj, + int bi, int bj, int ci, int cj, + int result, int ui, int uj, + int vi, int vj, int tactical_nodes, + char goal[MAX_BOARD][MAX_BOARD], + int goal_color); + + static int do_owl_attack(int m, int n, int *ui, int *uj, struct local_owl_data *owl, int komaster, int kom_i, int kom_j); @@ -761,7 +802,13 @@ int reading_nodes_when_called = get_reading_node_counter(); clock_t start = 0, end; double elapsed; + int tactical_nodes; + int temp_ui = -1, temp_uj = -1; + if (search_persistent_owl_cache(OWL_ATTACK, m, n, -1, -1, -1, -1, + &result, ui, uj, NULL, NULL)) + return result; + if (debug & DEBUG_OWL_PERFORMANCE) start = clock(); owl.local_owl_node_counter = 0; @@ -770,16 +817,23 @@ owl_mark_dragon(m, n, -1, -1, &owl); compute_owl_escape_values(&owl); owl_make_domains(&owl, NULL); - result = do_owl_attack(m, n, ui, uj, &owl, EMPTY, -1, -1); + result = do_owl_attack(m, n, &temp_ui, &temp_uj, &owl, EMPTY, -1, -1); + tactical_nodes = get_reading_node_counter() - reading_nodes_when_called; if (debug & DEBUG_OWL_PERFORMANCE) { end = clock(); elapsed = ((double) (end - start)) / CLOCKS_PER_SEC; gprintf("owl_attack %m, result %d %m (%d, %d nodes, %f seconds)\n", - m, n, result, *ui, *uj, owl.local_owl_node_counter, - get_reading_node_counter() - reading_nodes_when_called, elapsed); + m, n, result, temp_ui, temp_uj, owl.local_owl_node_counter, + tactical_nodes, elapsed); } + store_persistent_owl_cache(OWL_ATTACK, m, n, -1, -1, -1, -1, + result, temp_ui, temp_uj, -1, -1, + tactical_nodes, owl.goal, p[m][n]); + + if (ui) *ui = temp_ui; + if (uj) *uj = temp_uj; return result; } @@ -1218,7 +1272,14 @@ char saved_boundary[MAX_BOARD][MAX_BOARD]; clock_t start = 0, end; double elapsed; - + int tactical_nodes; + int temp_ui = -1, temp_uj = -1; + int temp_vi = -1, temp_vj = -1; + + if (search_persistent_owl_cache(OWL_THREATEN_ATTACK, m, n, -1, -1, -1, -1, + &result, ui, uj, vi, vj)) + return result; + if (debug & DEBUG_OWL_PERFORMANCE) start = clock(); owl.local_owl_node_counter = 0; @@ -1266,9 +1327,10 @@ break; } } - else if (do_owl_attack(m, n, vi, vj, &owl, EMPTY, -1, -1) == 1) { - if (ui) *ui = moves[k].i; - if (uj) *uj = moves[k].j; + else if (do_owl_attack(m, n, &temp_vi, &temp_vj, &owl, + EMPTY, -1, -1) == 1) { + temp_ui = moves[k].i; + temp_uj = moves[k].j; popgo(); assert (stackp == 0); result = 1; @@ -1279,14 +1341,25 @@ } } } + tactical_nodes = get_reading_node_counter() - reading_nodes_when_called; assert (stackp == 0); + if (debug & DEBUG_OWL_PERFORMANCE) { end = clock(); elapsed = ((double) (end - start)) / CLOCKS_PER_SEC; gprintf("owl_threaten_attack %m %m %m, result %d (%d, %d nodes, %f seconds)\n", - m, n, *ui, *uj, *vi, *vj, result, owl.local_owl_node_counter, - get_reading_node_counter() - reading_nodes_when_called, elapsed); + m, n, temp_ui, temp_uj, temp_vi, temp_vj, result, + owl.local_owl_node_counter, + tactical_nodes, elapsed); } + + store_persistent_owl_cache(OWL_THREATEN_ATTACK, m, n, -1, -1, -1, -1, + result, temp_ui, temp_uj, temp_vi, temp_vj, + tactical_nodes, owl.goal, p[m][n]); + if (ui) *ui = temp_ui; + if (uj) *uj = temp_uj; + if (vi) *vi = temp_vi; + if (vj) *vj = temp_vj; return result; } @@ -1318,6 +1391,12 @@ int reading_nodes_when_called = get_reading_node_counter(); clock_t start = 0, end; double elapsed; + int tactical_nodes; + int temp_ui = -1, temp_uj = -1; + + if (search_persistent_owl_cache(OWL_DEFEND, m, n, -1, -1, -1, -1, + &result, ui, uj, NULL, NULL)) + return result; if (debug & DEBUG_OWL_PERFORMANCE) start = clock(); @@ -1327,14 +1406,21 @@ owl_mark_dragon(m, n, -1, -1, &owl); compute_owl_escape_values(&owl); owl_make_domains(&owl, NULL); - result = do_owl_defend(m, n, ui, uj, &owl, EMPTY, -1, -1); + result = do_owl_defend(m, n, &temp_ui, &temp_uj, &owl, EMPTY, -1, -1); + tactical_nodes = get_reading_node_counter() - reading_nodes_when_called; if (debug & DEBUG_OWL_PERFORMANCE) { end = clock(); elapsed = ((double) (end - start)) / CLOCKS_PER_SEC; gprintf("owl_defend %m, result %d %m (%d, %d nodes, %f seconds)\n", - m, n, result, *ui, *uj, owl.local_owl_node_counter, - get_reading_node_counter() - reading_nodes_when_called, elapsed); + m, n, result, temp_ui, temp_uj, owl.local_owl_node_counter, + tactical_nodes, elapsed); } + + store_persistent_owl_cache(OWL_DEFEND, m, n, -1, -1, -1, -1, + result, temp_ui, temp_uj, -1, -1, + tactical_nodes, owl.goal, p[m][n]); + if (ui) *ui = temp_ui; + if (uj) *uj = temp_uj; return result; } @@ -1706,8 +1792,15 @@ int reading_nodes_when_called = get_reading_node_counter(); char saved_goal[MAX_BOARD][MAX_BOARD]; clock_t start = 0, end; - double elapsed; + int tactical_nodes; + int temp_ui = -1, temp_uj = -1; + int temp_vi = -1, temp_vj = -1; + + if (search_persistent_owl_cache(OWL_THREATEN_DEFENSE, m, n, -1, -1, -1, -1, + &result, ui, uj, vi, vj)) + return result; + if (debug & DEBUG_OWL_PERFORMANCE) start = clock(); owl.local_owl_node_counter = 0; @@ -1724,14 +1817,15 @@ if (trymove(moves[k].i, moves[k].j, color, moves[k].name, m, n)) { owl.lunches_are_current = 0; owl_update_goal(moves[k].i, moves[k].j, moves[k].same_dragon, &owl); - if (do_owl_defend(m, n, vi, vj, &owl, EMPTY, -1, -1) == 1) { - if (ui) *ui = moves[k].i; - if (uj) *uj = moves[k].j; + if (do_owl_defend(m, n, &temp_vi, &temp_vj, &owl, + EMPTY, -1, -1) == 1) { + temp_ui = moves[k].i; + temp_uj = moves[k].j; popgo(); /* Don't return the second move if occupied before trymove */ - if (vi && *vi != -1 && p[*vi][*vj] != EMPTY) { - *vi = -1; - *vj = -1; + if (temp_vi != -1 && p[temp_vi][temp_vj] != EMPTY) { + temp_vi = -1; + temp_vj = -1; } result = 1; break; @@ -1742,13 +1836,24 @@ } } } + tactical_nodes = get_reading_node_counter() - reading_nodes_when_called; + assert (stackp == 0); + if (debug & DEBUG_OWL_PERFORMANCE) { end = clock(); elapsed = ((double) (end - start)) / CLOCKS_PER_SEC; gprintf("owl_threaten_defense %m %m %m, result %d (%d, %d nodes, %f seconds)\n", - m, n, *ui, *uj, *vi, *vj, result, owl.local_owl_node_counter, - get_reading_node_counter() - reading_nodes_when_called, elapsed); + m, n, temp_ui, temp_uj, temp_vi, temp_vj, result, + owl.local_owl_node_counter, tactical_nodes, elapsed); } + + store_persistent_owl_cache(OWL_THREATEN_DEFENSE, m, n, -1, -1, -1, -1, + result, temp_ui, temp_uj, temp_vi, temp_vj, + tactical_nodes, owl.goal, p[m][n]); + if (ui) *ui = temp_ui; + if (uj) *uj = temp_uj; + if (vi) *vi = temp_vi; + if (vj) *vj = temp_vj; return result; } @@ -2671,9 +2776,15 @@ int result = 0; static struct local_owl_data owl; int reading_nodes_when_called = get_reading_node_counter(); + int tactical_nodes; owl.local_owl_node_counter = 0; TRACE("owl_does_defend %m %m\n", ti, tj, m, n); + + if (search_persistent_owl_cache(OWL_DOES_DEFEND, ti, tj, m, n, -1, -1, + &result, NULL, NULL, NULL, NULL)) + return result; + if (trymove(ti, tj, color, "owl_does_defend", m, n)) { owl.lunches_are_current = 0; owl_mark_dragon(m, n, -1, -1, &owl); @@ -2684,10 +2795,16 @@ owl.lunches_are_current = 0; popgo(); } + tactical_nodes = get_reading_node_counter() - reading_nodes_when_called; + DEBUG(DEBUG_OWL_PERFORMANCE, "owl_does_defend %m %m, result %d (%d, %d nodes)\n", - ti, tj, m, n, result, owl.local_owl_node_counter, - get_reading_node_counter() - reading_nodes_when_called); + ti, tj, m, n, result, owl.local_owl_node_counter, tactical_nodes); + + store_persistent_owl_cache(OWL_DOES_DEFEND, ti, tj, m, n, -1, -1, + result, -1, -1, -1, -1, + tactical_nodes, owl.goal, p[m][n]); + return result; } @@ -2743,12 +2860,17 @@ int color = p[ai][aj]; int result = 0; int reading_nodes_when_called = get_reading_node_counter(); + int tactical_nodes; static struct local_owl_data owl; assert(p[bi][bj] == color); TRACE("owl_connection_defends %m %m %m\n", ti, tj, ai, aj, bi, bj); + if (search_persistent_owl_cache(OWL_CONNECTION_DEFENDS, ti, tj, ai, aj, + bi, bj, &result, NULL, NULL, NULL, NULL)) + return result; + owl.local_owl_node_counter = 0; owl.lunches_are_current = 0; owl_mark_dragon(ai, aj, bi, bj, &owl); @@ -2761,11 +2883,17 @@ owl.lunches_are_current = 0; popgo(); } + tactical_nodes = get_reading_node_counter() - reading_nodes_when_called; DEBUG(DEBUG_OWL_PERFORMANCE, "owl_conn_defends %m %m %m, result %d (%d, %d nodes)\n", ti, tj, ai, aj, bi, bj, result, owl.local_owl_node_counter, - get_reading_node_counter() - reading_nodes_when_called); + tactical_nodes); + + store_persistent_owl_cache(OWL_CONNECTION_DEFENDS, ti, tj, ai, aj, bi, bj, + result, -1, -1, -1, -1, + tactical_nodes, owl.goal, color); + return result; } @@ -3044,7 +3172,11 @@ int m, n; int libi[MAX_SUBSTANTIAL_LIBS + 1], libj[MAX_SUBSTANTIAL_LIBS + 1]; int libs = findlib(i, j, MAX_SUBSTANTIAL_LIBS+1, libi, libj); + int reading_nodes_when_called = get_reading_node_counter(); + int tactical_nodes; static struct local_owl_data owl; + int result; + int ai, aj; owl.color = OTHER_COLOR(p[i][j]); owl.local_owl_node_counter = 0; @@ -3079,6 +3211,13 @@ } } + /* We must check the cache while stackp == 0, but we wait until the + * trivial tests have been done. + */ + if (search_persistent_owl_cache(OWL_SUBSTANTIAL, i, j, -1, -1, -1, -1, + &result, NULL, NULL, NULL, NULL)) + return result; + /* fill all the liberties */ for (k = 0; k < libs; k++) { if (trymove(libi[k], libj[k], owl.color, NULL, -1, -1)) @@ -3102,20 +3241,29 @@ compute_owl_escape_values(&owl); owl_mark_boundary(&owl); owl.lunches_are_current = 0; - { - int result; - int ai, aj; - if (do_owl_attack(libi[0], libj[0], &ai, &aj, &owl, EMPTY, -1, -1)) - result = 0; - else - result = 1; - while (stackp > 0) - popgo(); - return result; - } + + if (do_owl_attack(libi[0], libj[0], &ai, &aj, &owl, EMPTY, -1, -1)) + result = 0; + else + result = 1; + while (stackp > 0) + popgo(); + + tactical_nodes = get_reading_node_counter() - reading_nodes_when_called; + DEBUG(DEBUG_OWL_PERFORMANCE, + "owl_substantial %m, result %d (%d, %d nodes)\n", + i, j, result, owl.local_owl_node_counter, + tactical_nodes); + + store_persistent_owl_cache(OWL_SUBSTANTIAL, i, j, -1, -1, -1, -1, + result, -1, -1, -1, -1, + tactical_nodes, owl.goal, owl.color); + + return result; } + /* Returns true if and only if (i, j) is a 1-2 vertex, i.e. next to a * corner. */ @@ -3347,6 +3495,177 @@ return dragon_escape(owl->goal, owl->color, owl->escape_values); } +/*********************** + * Persistent owl cache + ***********************/ + +/* Returns 1 if the stored board is compatible with the current board, + * 0 otherwise. + */ +static int +verify_stored_board(char board[MAX_BOARD][MAX_BOARD]) +{ + int m, n; + for (m=0; m 0 && active[m-1][n] == k) + || (m < board_size-1 && active[m+1][n] == k) + || (n > 0 && active[m][n-1] == k) + || (n < board_size-1 && active[m][n+1] == k)) + active[m][n] = k + 1; + } + } + + for (m=0; m 0 && active[m-1][n] != 0 && p[m-1][n] != other) + || (m < board_size-1 && active[m+1][n] != 0 && p[m+1][n] != other) + || (n > 0 && active[m][n-1] != 0 && p[m][n-1] != other) + || (n < board_size-1 && active[m][n+1] != 0 && p[m][n+1] != other)) + active[m][n] = 1; + } + + for (m=0; m 0 ? p[m][n] : GRAY; + + persistent_owl_cache_size++; +} + + +/***********************/ /* Clear statistics. */ void