/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ * This is GNU Go, a Go program. Contact gnugo@gnu.org, or see * * http://www.gnu.org/software/gnugo/ for more information. * * * * Copyright 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, * * 2008 and 2009 by the Free Software Foundation. * * * * 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 - version 3 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 in file COPYING for more details. * * * * You should have received a copy of the GNU General Public * * License along with this program; if not, write to the Free * * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02111, USA. * \* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "gnugo.h" #include #include #include #include "liberty.h" #include "patterns.h" static void compute_effective_worm_sizes(void); static void do_compute_effective_worm_sizes(int color, int (*cw)[MAX_CLOSE_WORMS], int *ncw, int max_distance); static void compute_unconditional_status(void); static void find_worm_attacks_and_defenses(void); static void find_worm_threats(void); static int find_lunch(int str, int *lunch); static void change_tactical_point(int str, int move, int code, int points[MAX_TACTICAL_POINTS], int codes[MAX_TACTICAL_POINTS]); static void propagate_worm2(int str); static int genus(int str); static void markcomponent(int str, int pos, int mg[BOARDMAX]); static int examine_cavity(int pos, int *edge); static void cavity_recurse(int pos, int mx[BOARDMAX], int *border_color, int *edge, int str); static void ping_cave(int str, int *result1, int *result2, int *result3, int *result4); static void ping_recurse(int pos, int *counter, int mx[BOARDMAX], int mr[BOARDMAX], int color); static int touching(int pos, int color); static void find_attack_patterns(void); static void attack_callback(int anchor, int color, struct pattern *pattern, int ll, void *data); static void find_defense_patterns(void); static void defense_callback(int anchor, int color, struct pattern *pattern, int ll, void *data); static void build_worms(void); static void report_worm(int pos); /* A worm or string is a maximal connected set of stones of the same color, * black or white. * * Cavities are sets of connected empty vertices. */ /* make_worms() finds all worms and assembles some data about them. * * Each worm is marked with an origin. This is an arbitrarily chosen * element of the worm, in practice the algorithm puts the origin at * the first element when they are given the lexicographical order, * though its location is irrelevant for applications. To see if two * stones lie in the same worm, compare their origins. * * We will use the field dragon[ii].genus to keep track of * black- or white-bordered cavities (essentially eyes) which are found. * so this field must be zero'd now. */ void make_worms(void) { int pos; /* Build the basic worm data: color, origin, size, liberties. */ build_worms(); /* No point continuing if the board is completely empty. */ if (stones_on_board(BLACK | WHITE) == 0) return; /* Compute effective sizes of all worms. */ compute_effective_worm_sizes(); /* Look for unconditionally alive and dead worms, and unconditional * territory. */ compute_unconditional_status(); find_worm_attacks_and_defenses(); gg_assert(stackp == 0); /* Count liberties of different orders and initialize cutstone fields. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (IS_STONE(board[pos]) && is_worm_origin(pos, pos)) { int lib1, lib2, lib3, lib4; ping_cave(pos, &lib1, &lib2, &lib3, &lib4); ASSERT1(worm[pos].liberties == lib1, pos); worm[pos].liberties2 = lib2; worm[pos].liberties3 = lib3; worm[pos].liberties4 = lib4; worm[pos].cutstone = 0; worm[pos].cutstone2 = 0; propagate_worm(pos); } } gg_assert(stackp == 0); /* * There are two concepts of cutting stones in the worm array. * * worm.cutstone: * * A CUTTING STONE is one adjacent to two enemy strings, * which do not have a liberty in common. The most common * type of cutting string is in this situation. * * XO * OX * * A POTENTIAL CUTTING STONE is adjacent to two enemy * strings which do share a liberty. For example, X in: * * XO * O. * * For cutting strings we set worm[m][n].cutstone=2. For potential * cutting strings we set worm[m][n].cutstone=1. For other strings, * worm[m][n].cutstone=0. * * worm.cutstone2: * * Cutting points are identified by the patterns in the * connections database. Proper cuts are handled by the fact * that attacking and defending moves also count as moves * cutting or connecting the surrounding dragons. * * The cutstone field will now be set. The cutstone2 field is set * later, during find_cuts(), called from make_dragons(). * * We maintain both fields because the historically older cutstone * field is needed to deal with the fact that e.g. in the position * * * OXX.O * .OOXO * OXX.O * * the X stones are amalgamated into one dragon because neither cut * works as long as the two O stones are in atari. Therefore we add * one to the cutstone field for each potential cutting point, * indicating that these O stones are indeed worth rescuing. * * For the time being we use both concepts in parallel. It's * possible we also need the old concept for correct handling of lunches. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { int w1 = NO_MOVE; int w2 = NO_MOVE; int k; int pos2; /* Only work on each worm once. This is easiest done if we only * work with the origin of each worm. */ if (!IS_STONE(board[pos]) || !is_worm_origin(pos, pos)) continue; /* Try to find two adjacent worms (w1) and (w2) * of opposite colour from (pos). */ for (pos2 = BOARDMIN; pos2 < BOARDMAX; pos2++) { /* Work only with the opposite color from (pos). */ if (board[pos2] != OTHER_COLOR(board[pos])) continue; for (k = 0; k < 4; k++) { if (!ON_BOARD(pos2 + delta[k]) || worm[pos2 + delta[k]].origin != pos) continue; ASSERT1(board[pos2 + delta[k]] == board[pos], pos); /* If we have not already found a worm which meets the criteria, * store it into (w1), otherwise store it into (w2). */ if (w1 == NO_MOVE) w1 = worm[pos2].origin; else if (!is_same_worm(pos2, w1)) w2 = worm[pos2].origin; } } /* * We now verify the definition of cutting stones. We have * verified that the string at (pos) is adjacent to two enemy * strings at (w1) and (w2). We need to know if these * strings share a liberty. */ /* Only do this if we really found something. */ if (w2 != NO_MOVE) { worm[pos].cutstone = 2; if (count_common_libs(w1, w2) > 0) worm[pos].cutstone = 1; DEBUG(DEBUG_WORMS, "Worm at %1m has w1 %1m and w2 %1m, cutstone %d\n", pos, w1, w2, worm[pos].cutstone); } } gg_assert(stackp == 0); /* Set the genus of all worms. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (IS_STONE(board[pos]) && is_worm_origin(pos, pos)) { worm[pos].genus = genus(pos); propagate_worm(pos); } } gg_assert(stackp == 0); /* Now we try to improve the values of worm.attack and worm.defend. * If we find that capturing the string at str also defends the * string at str2, or attacks it, then we add points of attack and * defense. We don't add attacking point for strings that can't be * defended. */ { int color; int str; int moves_to_try[BOARDMAX]; memset(moves_to_try, 0, sizeof(moves_to_try)); /* Find which colors to try at what points. */ for (str = BOARDMIN; str < BOARDMAX; str++) { if (IS_STONE(board[str]) && is_worm_origin(str, str)) { color = board[str]; moves_to_try[worm[str].defense_points[0]] |= color; moves_to_try[worm[str].attack_points[0]] |= OTHER_COLOR(color); } } /* Loop over the board and over the colors and try the moves found * in the previous loop. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (!ON_BOARD(pos)) continue; for (color = WHITE; color <= BLACK; color++) { if (!(moves_to_try[pos] & color)) continue; /* Try to play color at pos and see what it leads to. */ if (!trymove(pos, color, "make_worms", NO_MOVE)) continue; /* We must read to the same depth that was used in the * initial determination of worm.attack and worm.defend * to avoid horizon effects. Since stackp has been * incremented we must also increment depth values. */ DEBUG(DEBUG_WORMS, "trying %1m\n", pos); increase_depth_values(); /* Now we try to find a group which is saved or attacked as well * by this move. */ for (str = BOARDMIN; str < BOARDMAX; str++) { if (!IS_STONE(board[str]) || !is_worm_origin(str, str)) continue; /* If the worm is of the opposite color to the move, * then we try to defend it. If there was a previous * attack and defense of it, and there is no defense * for the attack now... */ if (worm[str].color == OTHER_COLOR(color) && worm[str].attack_codes[0] != 0 && worm[str].defense_codes[0] != 0) { int dcode = find_defense(str, NULL); if (dcode < worm[str].defense_codes[0]) { int attack_works = 1; /* Sometimes find_defense() fails to find a * defense which has been found by other means. * Try if the old defense move still works. * * However, we first check if the _attack_ still exists, * because we could, for instance, drive the worm into * seki with our move. */ if (attack(str, NULL) >= worm[str].attack_codes[0]) { if (worm[str].defense_codes[0] != 0 && trymove(worm[str].defense_points[0], OTHER_COLOR(color), "make_worms", 0)) { int this_dcode = REVERSE_RESULT(attack(str, NULL)); if (this_dcode > dcode) { dcode = this_dcode; if (dcode >= worm[str].defense_codes[0]) attack_works = 0; } popgo(); } } else attack_works = 0; /* ...then add an attack point of that worm at pos. */ if (attack_works) { DEBUG(DEBUG_WORMS, "adding point of attack of %1m at %1m with code %d\n", str, pos, REVERSE_RESULT(dcode)); change_attack(str, pos, REVERSE_RESULT(dcode)); } } } /* If the worm is of the same color as the move we try to * attack it. If there previously was an attack on it, but * there is none now, then add a defense point of str at * pos. */ else if (worm[str].color == color && worm[str].attack_codes[0] != 0) { int acode = attack(str, NULL); if (acode < worm[str].attack_codes[0]) { int defense_works = 1; /* Sometimes attack() fails to find an * attack which has been found by other means. * Try if the old attack move still works. */ if (worm[str].attack_codes[0] != 0 && trymove(worm[str].attack_points[0], OTHER_COLOR(color), "make_worms", 0)) { int this_acode; if (board[str] == EMPTY) this_acode = WIN; else this_acode = REVERSE_RESULT(find_defense(str, NULL)); if (this_acode > acode) { acode = this_acode; if (acode >= worm[str].attack_codes[0]) defense_works = 0; } popgo(); } /* ...then add an attack point of that worm at pos. */ if (defense_works) { DEBUG(DEBUG_WORMS, "adding point of defense of %1m at %1m with code %d\n", str, pos, REVERSE_RESULT(acode)); change_defense(str, pos, REVERSE_RESULT(acode)); } } } } decrease_depth_values(); popgo(); } } } gg_assert(stackp == 0); /* Sometimes it happens that the tactical reading finds adjacent * strings which both can be attacked but not defended. (The reason * seems to be that the attacker tries harder to attack a string, * than the defender tries to capture it's neighbors.) When this * happens, the eyes code produces overlapping eye spaces and, still * worse, all the nondefendable stones actually get amalgamated with * their allies on the outside. * * To solve this we scan through the strings which can't be defended * and check whether they have a neighbor that can be attacked. In * this case we set the defense point of the former string to the * attacking point of the latter. * * Please notice that find_defense() will still read this out * incorrectly, which may lead to some confusion later. */ /* First look for vertical neighbors. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (IS_STONE(board[pos]) && IS_STONE(board[SOUTH(pos)]) && !is_same_worm(pos, SOUTH(pos))) { if (worm[pos].attack_codes[0] != 0 && worm[SOUTH(pos)].attack_codes[0] != 0) { if (worm[pos].defense_codes[0] == 0 && does_defend(worm[SOUTH(pos)].attack_points[0], pos)) { /* FIXME: need to check ko relationship here */ change_defense(pos, worm[SOUTH(pos)].attack_points[0], WIN); } if (worm[SOUTH(pos)].defense_codes[0] == 0 && does_defend(worm[pos].attack_points[0], SOUTH(pos))) { /* FIXME: need to check ko relationship here */ change_defense(SOUTH(pos), worm[pos].attack_points[0], WIN); } } } } /* Then look for horizontal neighbors. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (IS_STONE(board[pos]) && IS_STONE(board[EAST(pos)]) && !is_same_worm(pos, EAST(pos))) { if (worm[pos].attack_codes[0] != 0 && worm[EAST(pos)].attack_codes[0] != 0) { if (worm[pos].defense_codes[0] == 0 && does_defend(worm[EAST(pos)].attack_points[0], pos)) { /* FIXME: need to check ko relationship here */ change_defense(pos, worm[EAST(pos)].attack_points[0], WIN); } if (worm[EAST(pos)].defense_codes[0] == 0 && does_defend(worm[pos].attack_points[0], EAST(pos))) { /* FIXME: need to check ko relationship here */ change_defense(EAST(pos), worm[pos].attack_points[0], WIN); } } } } gg_assert(stackp == 0); /* Find adjacent worms that can be easily captured, aka lunches. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { int lunch; if (!IS_STONE(board[pos]) || !is_worm_origin(pos, pos)) continue; if (find_lunch(pos, &lunch) && (worm[lunch].attack_codes[0] == WIN || worm[lunch].attack_codes[0] == KO_A)) { DEBUG(DEBUG_WORMS, "lunch found for %1m at %1m\n", pos, lunch); worm[pos].lunch = lunch; } else worm[pos].lunch = NO_MOVE; propagate_worm(pos); } if (!disable_threat_computation) find_worm_threats(); /* Identify INESSENTIAL strings. * * These are defined as surrounded strings which have no life * potential unless part of their surrounding chain can be captured. * We give a conservative definition of inessential: * - the genus must be zero * - there can no second order liberties * - there can be no more than two edge liberties * - if it is removed from the board, the remaining cavity has * border color the opposite color of the string * - it contains at most two edge vertices. * * If we get serious about identifying seki, we might want to add: * * - if it has fewer than 4 liberties it is tactically dead. * * The last condition is helpful in excluding strings which are * alive in seki. * * An inessential string can be thought of as residing inside the * opponent's eye space. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (IS_STONE(board[pos]) && worm[pos].origin == pos && worm[pos].genus == 0 && worm[pos].liberties2 == 0 && !worm[pos].cutstone && worm[pos].lunch == NO_MOVE) { int edge; int border_color = examine_cavity(pos, &edge); if (border_color != GRAY && edge < 3) { DEBUG(DEBUG_WORMS, "Worm %1m identified as inessential.\n", pos); worm[pos].inessential = 1; propagate_worm(pos); } } } } /* * Clear all worms and initialize the basic data fields: * color, origin, size, liberties * This is a substep of make_worms(). */ static void build_worms() { int pos; /* Set all worm data fields to 0. */ memset(worm, 0 , sizeof(worm)); /* Initialize the worm data for each worm. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) if (ON_BOARD(pos)) worm[pos].origin = NO_MOVE; for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (!ON_BOARD(pos) || worm[pos].origin != NO_MOVE) continue; worm[pos].color = board[pos]; worm[pos].origin = pos; worm[pos].inessential = 0; worm[pos].invincible = 0; worm[pos].unconditional_status = UNKNOWN; worm[pos].effective_size = 0.0; if (IS_STONE(board[pos])) { worm[pos].liberties = countlib(pos); worm[pos].size = countstones(pos); propagate_worm(pos); } } } /* Compute effective size of each worm. * * Effective size is the number of stones in a worm plus half the * number of empty intersections that are at least as close to this * worm as to any other worm. This is used to estimate the direct * territorial value of capturing a worm. Intersections that are * shared are counted with equal fractional values for each worm. * * We never count intersections further away than distance 3. * * This function is also used to compute arrays with information about * the distances to worms of both or either color. In the latter case * we count intersections up to a distance of 5. */ static void compute_effective_worm_sizes() { do_compute_effective_worm_sizes(BLACK | WHITE, close_worms, number_close_worms, 3); do_compute_effective_worm_sizes(BLACK, close_black_worms, number_close_black_worms, 5); do_compute_effective_worm_sizes(WHITE, close_white_worms, number_close_white_worms, 5); } static void do_compute_effective_worm_sizes(int color, int (*cw)[MAX_CLOSE_WORMS], int *ncw, int max_distance) { int pos; /* Distance to closest worm, -1 means unassigned, 0 that there is * a stone at the location, 1 a liberty of a stone, and so on. */ int distance[BOARDMAX]; /* Pointer to the origin of the closest worms. A very large number of * worms may potentially be equally close, but no more than * 2*(board_size-1). */ static int worms[BOARDMAX][2*(MAX_BOARD-1)]; int nworms[BOARDMAX]; /* number of equally close worms */ int found_one; int dist; /* current distance */ int k, l; int r; /* Initialize arrays. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (!ON_BOARD(pos)) continue; for (k = 0; k < 2*(board_size-1); k++) worms[pos][k] = NO_MOVE; nworms[pos] = 0; if (board[pos] & color) { distance[pos] = 0; worms[pos][0] = worm[pos].origin; nworms[pos]++; } else distance[pos] = -1; } dist = 0; found_one = 1; while (found_one && dist <= max_distance) { found_one = 0; dist++; for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (!ON_BOARD(pos) || distance[pos] != -1) continue; /* already claimed */ for (r = 0; r < 4; r++) { int pos2 = pos + delta[r]; if (ON_BOARD(pos2) && distance[pos2] == dist - 1) { found_one = 1; distance[pos] = dist; for (k = 0; k < nworms[pos2]; k++) { int already_counted = 0; for (l = 0; l < nworms[pos]; l++) if (worms[pos][l] == worms[pos2][k]) { already_counted = 1; break; } if (!already_counted) { ASSERT1(nworms[pos] < 2*(board_size-1), pos); worms[pos][nworms[pos]] = worms[pos2][k]; nworms[pos]++; } } } } } } /* Compute the effective sizes but only when all worms are considered. */ if (color == (BLACK | WHITE)) { /* Distribute (fractional) contributions to the worms. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (!ON_BOARD(pos)) continue; for (k = 0; k < nworms[pos]; k++) { int w = worms[pos][k]; if (board[pos] == EMPTY) worm[w].effective_size += 0.5/nworms[pos]; else worm[w].effective_size += 1.0; } } /* Propagate the effective size values all over the worms. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) if (IS_STONE(board[pos]) && is_worm_origin(pos, pos)) propagate_worm(pos); } /* Fill in the appropriate close_*_worms (cw) and * number_close_*_worms (ncw) arrays. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (!ON_BOARD(pos)) continue; if (nworms[pos] > MAX_CLOSE_WORMS) ncw[pos] = 0; else ncw[pos] = nworms[pos]; for (k = 0; k < ncw[pos]; k++) cw[pos][k] = worms[pos][k]; } } /* Identify worms which are unconditionally uncapturable in the * strongest sense, i.e. even if the opponent is allowed an arbitrary * number of consecutive moves. Also identify worms which are * similarly unconditionally dead and empty points which are * unconditional territory for either player. */ static void compute_unconditional_status() { int unconditional_territory[BOARDMAX]; int pos; int color; for (color = WHITE; color <= BLACK; color++) { unconditional_life(unconditional_territory, color); if (get_level() >= 10) find_unconditionally_meaningless_moves(unconditional_territory, color); for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (!ON_BOARD(pos) || !unconditional_territory[pos]) continue; if (board[pos] == color) { worm[pos].unconditional_status = ALIVE; if (unconditional_territory[pos] == 1) worm[pos].invincible = 1; } else if (board[pos] == EMPTY) { if (color == WHITE) worm[pos].unconditional_status = WHITE_TERRITORY; else worm[pos].unconditional_status = BLACK_TERRITORY; } else worm[pos].unconditional_status = DEAD; } } gg_assert(stackp == 0); } /* * Analyze tactical safety of each worm. */ static void find_worm_attacks_and_defenses() { int str; int k; int acode, dcode; int attack_point; int defense_point; static int libs[MAXLIBS]; int liberties; int color; int other; /* 1. Start with finding attack points. */ for (str = BOARDMIN; str < BOARDMAX; str++) { if (!IS_STONE(board[str]) || !is_worm_origin(str, str)) continue; TRACE("considering attack of %1m\n", str); /* Initialize all relevant fields at once. */ for (k = 0; k < MAX_TACTICAL_POINTS; k++) { worm[str].attack_codes[k] = 0; worm[str].attack_points[k] = 0; worm[str].defense_codes[k] = 0; worm[str].defense_points[k] = 0; } propagate_worm(str); acode = attack(str, &attack_point); if (acode != 0) { DEBUG(DEBUG_WORMS, "worm at %1m can be attacked at %1m\n", str, attack_point); change_attack(str, attack_point, acode); } } gg_assert(stackp == 0); /* 2. Use pattern matching to find a few more attacks. */ find_attack_patterns(); gg_assert(stackp == 0); /* 3. Now find defense moves. */ for (str = BOARDMIN; str < BOARDMAX; str++) { if (!IS_STONE(board[str]) || !is_worm_origin(str, str)) continue; if (worm[str].attack_codes[0] != 0) { TRACE("considering defense of %1m\n", str); dcode = find_defense(str, &defense_point); if (dcode != 0) { TRACE("worm at %1m can be defended at %1m\n", str, defense_point); if (defense_point != NO_MOVE) change_defense(str, defense_point, dcode); } else { /* If the point of attack is not adjacent to the worm, * it is possible that this is an overlooked point of * defense, so we try and see if it defends. */ attack_point = worm[str].attack_points[0]; if (!liberty_of_string(attack_point, str)) if (trymove(attack_point, worm[str].color, "make_worms", NO_MOVE)) { int acode = attack(str, NULL); if (acode != WIN) { change_defense(str, attack_point, REVERSE_RESULT(acode)); TRACE("worm at %1m can be defended at %1m with code %d\n", str, attack_point, REVERSE_RESULT(acode)); } popgo(); } } } } gg_assert(stackp == 0); /* 4. Use pattern matching to find a few more defense moves. */ find_defense_patterns(); gg_assert(stackp == 0); /* * 5. Find additional attacks and defenses by testing all immediate * liberties. Further attacks and defenses are found by pattern * matching and by trying whether each attack or defense point * attacks or defends other strings. */ for (str = BOARDMIN; str < BOARDMAX; str++) { color = board[str]; if (!IS_STONE(color) || !is_worm_origin(str, str)) continue; other = OTHER_COLOR(color); if (worm[str].attack_codes[0] == 0) continue; /* There is at least one attack on this group. Try the * liberties. */ liberties = findlib(str, MAXLIBS, libs); for (k = 0; k < liberties; k++) { int pos = libs[k]; if (!attack_move_known(pos, str)) { /* Try to attack on the liberty. Don't consider * send-two-return-one moves. */ if (!send_two_return_one(pos, other) && trymove(pos, other, "make_worms", str)) { if (board[str] == EMPTY || attack(str, NULL)) { if (board[str] == EMPTY) dcode = 0; else dcode = find_defense(str, NULL); if (dcode != WIN) change_attack(str, pos, REVERSE_RESULT(dcode)); } popgo(); } } /* Try to defend at the liberty. */ if (!defense_move_known(pos, str)) { if (worm[str].defense_codes[0] != 0) if (trymove(pos, color, "make_worms", NO_MOVE)) { acode = attack(str, NULL); if (acode != WIN) change_defense(str, pos, REVERSE_RESULT(acode)); popgo(); } } } } gg_assert(stackp == 0); } /* * Find moves threatening to attack or save all worms. */ static void find_worm_threats() { int str; static int libs[MAXLIBS]; int liberties; int k; int l; int color; for (str = BOARDMIN; str < BOARDMAX; str++) { color = board[str]; if (!IS_STONE(color) || !is_worm_origin(str, str)) continue; /* 1. Start with finding attack threats. */ /* Only try those worms that have no attack. */ if (worm[str].attack_codes[0] == 0) { attack_threats(str, MAX_TACTICAL_POINTS, worm[str].attack_threat_points, worm[str].attack_threat_codes); #if 0 /* Threaten to attack by saving weak neighbors. */ num_adj = chainlinks(str, adjs); for (k = 0; k < num_adj; k++) { if (worm[adjs[k]].attack_codes[0] != 0 && worm[adjs[k]].defense_codes[0] != 0) for (r = 0; r < MAX_TACTICAL_POINTS; r++) { int bb; if (worm[adjs[k]].defense_codes[r] == 0) break; bb = worm[adjs[k]].defense_points[r]; if (trymove(bb, other, "threaten attack", str, EMPTY, NO_MOVE)) { int acode; if (board[str] == EMPTY) acode = WIN; else acode = attack(str, NULL); if (acode != 0) change_attack_threat(str, bb, acode); popgo(); } } } #endif /* FIXME: Try other moves also (patterns?). */ } /* 2. Continue with finding defense threats. */ /* Only try those worms that have an attack. */ if (worm[str].attack_codes[0] != 0 && worm[str].defense_codes[0] == 0) { liberties = findlib(str, MAXLIBS, libs); for (k = 0; k < liberties; k++) { int aa = libs[k]; /* Try to threaten on the liberty. */ if (trymove(aa, color, "threaten defense", NO_MOVE)) { if (attack(str, NULL) == WIN) { int dcode = find_defense(str, NULL); if (dcode != 0) change_defense_threat(str, aa, dcode); } popgo(); } /* Try to threaten on second order liberties. */ for (l = 0; l < 4; l++) { int bb = libs[k] + delta[l]; if (!ON_BOARD(bb) || IS_STONE(board[bb]) || liberty_of_string(bb, str)) continue; if (trymove(bb, color, "threaten defense", str)) { if (attack(str, NULL) == WIN) { int dcode = find_defense(str, NULL); if (dcode != 0) change_defense_threat(str, bb, dcode); } popgo(); } } } /* It might be interesting to look for defense threats by * attacking weak neighbors, similar to threatening attack by * defending a weak neighbor. However, in this case it seems * probable that if there is such an attack, it's a real * defense, not only a threat. */ /* FIXME: Try other moves also (patterns?). */ } } } /* find_lunch(str, &worm) looks for a worm adjoining the * string at (str) which can be easily captured. Whether or not it can * be defended doesn't matter. * * Returns the location of the string in (*lunch). */ static int find_lunch(int str, int *lunch) { int pos; int k; ASSERT1(IS_STONE(board[str]), str); ASSERT1(stackp == 0, str); *lunch = NO_MOVE; for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (board[pos] != OTHER_COLOR(board[str])) continue; for (k = 0; k < 8; k++) { int apos = pos + delta[k]; if (ON_BOARD(apos) && is_same_worm(apos, str)) { if (worm[pos].attack_codes[0] != 0 && !is_ko_point(pos)) { /* * If several adjacent lunches are found, we pick the * juiciest. First maximize cutstone, then minimize liberties. * We can only do this if the worm data is available, * i.e. if stackp==0. */ if (*lunch == NO_MOVE || worm[pos].cutstone > worm[*lunch].cutstone || (worm[pos].cutstone == worm[*lunch].cutstone && worm[pos].liberties < worm[*lunch].liberties)) { *lunch = worm[pos].origin; } } break; } } } if (*lunch != NO_MOVE) return 1; return 0; } /* * Test whether two worms are the same. Used by autohelpers. * Before this function can be called, build_worms must have been run. */ int is_same_worm(int w1, int w2) { return worm[w1].origin == worm[w2].origin; } /* * Test whether the origin of the worm at (w) is (pos). */ int is_worm_origin(int w, int pos) { return worm[w].origin == pos; } /* * change_defense(str, move, dcode) is used to add and remove defense * points. It can also be used to change the defense code. The meaning * of the call is that the string (str) can be defended by (move) with * defense code (dcode). If (dcode) is zero, the move is removed from * the list of defense moves if it was previously listed. */ void change_defense(int str, int move, int dcode) { str = worm[str].origin; change_tactical_point(str, move, dcode, worm[str].defense_points, worm[str].defense_codes); } /* * change_attack(str, move, acode) is used to add and remove attack * points. It can also be used to change the attack code. The meaning * of the call is that the string (str) can be attacked by (move) with * attack code (acode). If (acode) is zero, the move is removed from * the list of attack moves if it was previously listed. */ void change_attack(int str, int move, int acode) { str = worm[str].origin; DEBUG(DEBUG_WORMS, "change_attack: %1m %1m %d\n", str, move, acode); change_tactical_point(str, move, acode, worm[str].attack_points, worm[str].attack_codes); } /* * change_defense_threat(str, move, dcode) is used to add and remove * defense threat points. It can also be used to change the defense * threat code. The meaning of the call is that the string (str) can * threaten to be defended by (move) with defense threat code (dcode). * If (dcode) is zero, the move is removed from the list of defense * threat moves if it was previously listed. */ void change_defense_threat(int str, int move, int dcode) { str = worm[str].origin; change_tactical_point(str, move, dcode, worm[str].defense_threat_points, worm[str].defense_threat_codes); } /* * change_attack_threat(str, move, acode) is used to add and remove * attack threat points. It can also be used to change the attack * threat code. The meaning of the call is that the string (str) can * threaten to be attacked by (move) with attack threat code (acode). * If (acode) is zero, the move is removed from the list of attack * threat moves if it was previously listed. */ void change_attack_threat(int str, int move, int acode) { str = worm[str].origin; change_tactical_point(str, move, acode, worm[str].attack_threat_points, worm[str].attack_threat_codes); } /* Check whether (move) is listed as an attack point for (str) and * return the attack code. If (move) is not listed, return 0. */ int attack_move_known(int move, int str) { return movelist_move_known(move, MAX_TACTICAL_POINTS, worm[str].attack_points, worm[str].attack_codes); } /* Check whether (move) is listed as a defense point for (str) and * return the defense code. If (move) is not listed, return 0. */ int defense_move_known(int move, int str) { return movelist_move_known(move, MAX_TACTICAL_POINTS, worm[str].defense_points, worm[str].defense_codes); } /* Check whether (move) is listed as an attack threat point for (str) * and return the attack threat code. If (move) is not listed, return * 0. */ int attack_threat_move_known(int move, int str) { return movelist_move_known(move, MAX_TACTICAL_POINTS, worm[str].attack_threat_points, worm[str].attack_threat_codes); } /* Check whether (move) is listed as a defense threat point for (str) * and return the defense threat code. If (move) is not listed, return * 0. */ int defense_threat_move_known(int move, int str) { return movelist_move_known(move, MAX_TACTICAL_POINTS, worm[str].defense_threat_points, worm[str].defense_threat_codes); } /* * This function does the real work for change_attack(), * change_defense(), change_attack_threat(), and * change_defense_threat(). */ static void change_tactical_point(int str, int move, int code, int points[MAX_TACTICAL_POINTS], int codes[MAX_TACTICAL_POINTS]) { ASSERT_ON_BOARD1(str); ASSERT1(str == worm[str].origin, str); movelist_change_point(move, code, MAX_TACTICAL_POINTS, points, codes); propagate_worm2(str); } /* * propagate_worm() takes the worm data at one stone and copies it to * the remaining members of the worm. * * Even though we don't need to copy all the fields, it's probably * better to do a structure copy which should compile to a block copy. */ void propagate_worm(int pos) { int k; int num_stones; int stones[MAX_BOARD * MAX_BOARD]; gg_assert(stackp == 0); ASSERT1(IS_STONE(board[pos]), pos); num_stones = findstones(pos, MAX_BOARD * MAX_BOARD, stones); for (k = 0; k < num_stones; k++) if (stones[k] != pos) worm[stones[k]] = worm[pos]; } /* * propagate_worm2() is a relative to propagate_worm() which can be * used when stackp>0 but not for the initial construction of the * worms. */ static void propagate_worm2(int str) { int pos; ASSERT_ON_BOARD1(str); ASSERT1(IS_STONE(worm[str].color), str); for (pos = BOARDMIN; pos < BOARDMAX; pos++) if (board[pos] == board[str] && is_same_worm(pos, str) && pos != str) worm[pos] = worm[str]; } /* Report all known attack, defense, attack threat, and defense threat * moves. But limit this to the moves which can be made by (color). */ void worm_reasons(int color) { int pos; int k; for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (!ON_BOARD(pos) || board[pos] == EMPTY) continue; if (!is_worm_origin(pos, pos)) continue; if (board[pos] == OTHER_COLOR(color)) { for (k = 0; k < MAX_TACTICAL_POINTS; k++) { if (worm[pos].attack_codes[k] != 0) add_attack_move(worm[pos].attack_points[k], pos, worm[pos].attack_codes[k]); if (worm[pos].attack_threat_codes[k] != 0) add_attack_threat_move(worm[pos].attack_threat_points[k], pos, worm[pos].attack_threat_codes[k]); } } if (board[pos] == color) { for (k = 0; k < MAX_TACTICAL_POINTS; k++) { if (worm[pos].defense_codes[k] != 0) add_defense_move(worm[pos].defense_points[k], pos, worm[pos].defense_codes[k]); if (worm[pos].defense_threat_codes[k] != 0) add_defense_threat_move(worm[pos].defense_threat_points[k], pos, worm[pos].defense_threat_codes[k]); } } } } /* ping_cave(str, *lib1, ...) is applied when (str) points to a string. * It computes the vector (*lib1, *lib2, *lib3, *lib4), * where *lib1 is the number of liberties of the string, * *lib2 is the number of second order liberties (empty vertices * at distance two) and so forth. * * The definition of liberties of order >1 is adapted to the problem * of detecting the shape of the surrounding cavity. In particular * we want to be able to see if a group is loosely surrounded. * * A liberty of order n is an empty space which may be connected * to the string by placing n stones of the same color on the board, * but no fewer. The path of connection may pass through an intervening group * of the same color. The stones placed at distance >1 may not touch a * group of the opposite color. At the edge, also diagonal neighbors * count as touching. The path may also not pass through a liberty at distance * 1 if that liberty is flanked by two stones of the opposing color. This * reflects the fact that the O stone is blocked from expansion to the * left by the two X stones in the following situation: * * X. * .O * X. * * On the edge, one stone is sufficient to block expansion: * * X. * .O * -- */ static void ping_cave(int str, int *lib1, int *lib2, int *lib3, int *lib4) { int pos; int k; int libs[MAXLIBS]; int mrc[BOARDMAX]; int mse[BOARDMAX]; int color = board[str]; int other = OTHER_COLOR(color); memset(mse, 0, sizeof(mse)); /* Find and mark the first order liberties. */ *lib1 = findlib(str, MAXLIBS, libs); for (k = 0; k < *lib1; k++) mse[libs[k]] = 1; /* Reset mse at liberties which are flanked by two stones of the * opposite color, or one stone and the edge. */ for (pos = BOARDMIN; pos < BOARDMAX; pos++) if (ON_BOARD(pos) && mse[pos] && ((( !ON_BOARD(SOUTH(pos)) || board[SOUTH(pos)] == other) && ( !ON_BOARD(NORTH(pos)) || board[NORTH(pos)] == other)) || (( !ON_BOARD(WEST(pos)) || board[WEST(pos)] == other) && (!ON_BOARD(EAST(pos)) || board[EAST(pos)] == other)))) mse[pos] = 0; *lib2 = 0; memset(mrc, 0, sizeof(mrc)); ping_recurse(str, lib2, mse, mrc, color); *lib3 = 0; memset(mrc, 0, sizeof(mrc)); ping_recurse(str, lib3, mse, mrc, color); *lib4 = 0; memset(mrc, 0, sizeof(mrc)); ping_recurse(str, lib4, mse, mrc, color); } /* recursive function called by ping_cave */ static void ping_recurse(int pos, int *counter, int mx[BOARDMAX], int mr[BOARDMAX], int color) { int k; mr[pos] = 1; for (k = 0; k < 4; k++) { int apos = pos + delta[k]; if (board[apos] == EMPTY && mx[apos] == 0 && mr[apos] == 0 && !touching(apos, OTHER_COLOR(color))) { (*counter)++; mr[apos] = 1; mx[apos] = 1; } } if (!is_ko_point(pos)) { for (k = 0; k < 4; k++) { int apos = pos + delta[k]; if (ON_BOARD(apos) && mr[apos] == 0 && (mx[apos] == 1 || board[apos] == color)) ping_recurse(apos, counter, mx, mr, color); } } } /* touching(pos, color) returns true if the vertex at (pos) is * touching any stone of (color). */ static int touching(int pos, int color) { return (board[SOUTH(pos)] == color || board[WEST(pos)] == color || board[NORTH(pos)] == color || board[EAST(pos)] == color); } /* The GENUS of a string is the number of connected components of * its complement, minus one. It is an approximation to the number of * eyes of the string. */ static int genus(int str) { int pos; int mg[BOARDMAX]; int gen = -1; memset(mg, 0, sizeof(mg)); for (pos = BOARDMIN; pos < BOARDMAX; pos++) { if (ON_BOARD(pos) && !mg[pos] && (board[pos] == EMPTY || !is_same_worm(pos, str))) { markcomponent(str, pos, mg); gen++; } } return gen; } /* This recursive function marks the component at (pos) of * the complement of the string with origin (str) */ static void markcomponent(int str, int pos, int mg[BOARDMAX]) { int k; mg[pos] = 1; for (k = 0; k < 4; k++) { int apos = pos + delta[k]; if (ON_BOARD(apos) && mg[apos] == 0 && (board[apos] == EMPTY || !is_same_worm(apos, str))) markcomponent(str, apos, mg); } } /* examine_cavity(pos, *edge), if (pos) is EMPTY, examines the * cavity at (m, n) and returns its bordercolor, * which can be BLACK, WHITE or GRAY. The edge parameter is set to the * number of edge vertices in the cavity. * * If (pos) is nonempty, it returns the same result, imagining * that the string at (pos) is removed. The edge parameter is * set to the number of vertices where the cavity meets the * edge in a point outside the removed string. */ static int examine_cavity(int pos, int *edge) { int border_color = EMPTY; int ml[BOARDMAX]; int origin = NO_MOVE; ASSERT_ON_BOARD1(pos); gg_assert(edge != NULL); memset(ml, 0, sizeof(ml)); *edge = 0; if (IS_STONE(board[pos])) origin = find_origin(pos); cavity_recurse(pos, ml, &border_color, edge, origin); if (border_color != EMPTY) return border_color; /* We should have returned now, unless the board is completely empty. * Verify that this is the case and then return GRAY. * * Notice that the board appears completely empty if there's only a * single string and pos points to it. */ gg_assert(border_color == EMPTY && ((pos == NO_MOVE && stones_on_board(BLACK | WHITE) == 0) || (pos != NO_MOVE && stones_on_board(BLACK | WHITE) == countstones(pos)))); return GRAY; } /* helper function for examine_cavity. * border_color contains information so far : transitions allowed are * EMPTY -> BLACK/WHITE * BLACK/WHITE -> BLACK | WHITE * * mx[pos] is 1 if (pos) has already been visited. * * if (str) points to the origin of a string, it will be ignored. * * On (fully-unwound) exit * *border_color should be BLACK, WHITE or BLACK | WHITE * *edge is the count of edge pieces * * *border_color should be EMPTY if and only if the board * is completely empty or only contains the ignored string. */ static void cavity_recurse(int pos, int mx[BOARDMAX], int *border_color, int *edge, int str) { int k; ASSERT1(mx[pos] == 0, pos); mx[pos] = 1; if (is_edge_vertex(pos) && board[pos] == EMPTY) (*edge)++; /* Loop over the four neighbors. */ for (k = 0; k < 4; k++) { int apos = pos + delta[k]; if (ON_BOARD(apos) && !mx[apos]) { int neighbor_empty = 0; if (board[apos] == EMPTY) neighbor_empty = 1; else { /* Count the neighbor as empty if it is part of the (ai, aj) string. */ if (str == find_origin(apos)) neighbor_empty = 1; else neighbor_empty = 0; } if (!neighbor_empty) *border_color |= board[apos]; else cavity_recurse(apos, mx, border_color, edge, str); } } } /* Find attacking moves by pattern matching, for both colors. */ static void find_attack_patterns(void) { matchpat(attack_callback, ANCHOR_OTHER, &attpat_db, NULL, NULL); } /* Try to attack every X string in the pattern, whether there is an attack * before or not. Only exclude already known attacking moves. */ static void attack_callback(int anchor, int color, struct pattern *pattern, int ll, void *data) { int move; int k; UNUSED(data); move = AFFINE_TRANSFORM(pattern->move_offset, ll, anchor); /* If the pattern has a constraint, call the autohelper to see * if the pattern must be rejected. */ if (pattern->autohelper_flag & HAVE_CONSTRAINT) { if (!pattern->autohelper(ll, move, color, 0)) return; } /* If the pattern has a helper, call it to see if the pattern must * be rejected. */ if (pattern->helper) { if (!pattern->helper(pattern, ll, move, color)) { DEBUG(DEBUG_WORMS, "Attack pattern %s+%d rejected by helper at %1m\n", pattern->name, ll, move); return; } } /* Loop through pattern elements in search of X strings to attack. */ for (k = 0; k < pattern->patlen; ++k) { /* match each point */ if (pattern->patn[k].att == ATT_X) { /* transform pattern real coordinate */ int pos = AFFINE_TRANSFORM(pattern->patn[k].offset, ll, anchor); int str = worm[pos].origin; /* A string with 5 liberties or more is considered tactically alive. */ if (countlib(str) > 4) continue; if (attack_move_known(move, str)) continue; /* No defenses are known at this time, so defend_code is always 0. */ #if 0 /* If the string can be attacked but not defended, ignore it. */ if (worm[str].attack_codes[0] == WIN && worm[str].defense_codes[0] == 0) continue; #endif /* FIXME: Don't attack the same string more than once. * Play (move) and see if there is a defense. */ if (trymove(move, color, "attack_callback", str)) { int dcode; if (!board[str]) dcode = 0; else if (!attack(str, NULL)) dcode = WIN; else dcode = find_defense(str, NULL); popgo(); /* Do not pick up suboptimal attacks at this time. Since we * don't know whether the string can be defended it's quite * possible that it only has a ko defense and then we would * risk to find an irrelevant move to attack with ko. */ if (dcode != WIN && REVERSE_RESULT(dcode) >= worm[str].attack_codes[0]) { change_attack(str, move, REVERSE_RESULT(dcode)); DEBUG(DEBUG_WORMS, "Attack pattern %s+%d found attack on %1m at %1m with code %d\n", pattern->name, ll, str, move, REVERSE_RESULT(dcode)); } } } } } static void find_defense_patterns(void) { matchpat(defense_callback, ANCHOR_COLOR, &defpat_db, NULL, NULL); } static void defense_callback(int anchor, int color, struct pattern *pattern, int ll, void *data) { int move; int k; UNUSED(data); move = AFFINE_TRANSFORM(pattern->move_offset, ll, anchor); /* If the pattern has a constraint, call the autohelper to see * if the pattern must be rejected. */ if (pattern->autohelper_flag & HAVE_CONSTRAINT) { if (!pattern->autohelper(ll, move, color, 0)) return; } /* If the pattern has a helper, call it to see if the pattern must * be rejected. */ if (pattern->helper) { if (!pattern->helper(pattern, ll, move, color)) { DEBUG(DEBUG_WORMS, "Defense pattern %s+%d rejected by helper at %1m\n", pattern->name, ll, move); return; } } /* Loop through pattern elements in search for O strings to defend. */ for (k = 0; k < pattern->patlen; ++k) { /* match each point */ if (pattern->patn[k].att == ATT_O) { /* transform pattern real coordinate */ int pos = AFFINE_TRANSFORM(pattern->patn[k].offset, ll, anchor); int str = worm[pos].origin; if (worm[str].attack_codes[0] == 0 || defense_move_known(move, str)) continue; /* FIXME: Don't try to defend the same string more than once. * FIXME: For all attacks on this string, we should test whether * the proposed move happens to refute the attack. * Play (move) and see if there is an attack. */ if (trymove(move, color, "defense_callback", str)) { int acode = attack(str, NULL); popgo(); if (acode < worm[str].attack_codes[0]) { change_defense(str, move, REVERSE_RESULT(acode)); DEBUG(DEBUG_WORMS, "Defense pattern %s+%d found defense of %1m at %1m with code %d\n", pattern->name, ll, str, move, REVERSE_RESULT(acode)); } } } } } void get_lively_stones(int color, signed char safe_stones[BOARDMAX]) { int pos; memset(safe_stones, 0, BOARDMAX * sizeof(*safe_stones)); for (pos = BOARDMIN; pos < BOARDMAX; pos++) if (IS_STONE(board[pos]) && find_origin(pos) == pos) { if ((stackp == 0 && worm[pos].attack_codes[0] == 0) || !attack(pos, NULL) || (board[pos] == color && ((stackp == 0 && worm[pos].defense_codes[0] != 0) || find_defense(pos, NULL)))) mark_string(pos, safe_stones, 1); } } void compute_worm_influence() { signed char safe_stones[BOARDMAX]; get_lively_stones(BLACK, safe_stones); compute_influence(BLACK, safe_stones, NULL, &initial_black_influence, NO_MOVE, "initial black influence"); get_lively_stones(WHITE, safe_stones); compute_influence(WHITE, safe_stones, NULL, &initial_white_influence, NO_MOVE, "initial white influence"); } /* ================================================================ */ /* Debugger functions */ /* ================================================================ */ /* For use in gdb, print details of the worm at (m, n). * Add this to your .gdbinit file: * * define worm * set ascii_report_worm("$arg0") * end * * Now 'worm S8' will report the details of the S8 worm. * */ void ascii_report_worm(char *string) { int pos = string_to_location(board_size, string); report_worm(pos); } static void report_worm(int pos) { int i; if (board[pos] == EMPTY) { gprintf("There is no worm at %1m\n", pos); return; } gprintf("*** worm at %1m:\n", pos); gprintf("color: %s; origin: %1m; size: %d; effective size: %f\n", (worm[pos].color == WHITE) ? "White" : "Black", worm[pos].origin, worm[pos].size, worm[pos].effective_size); gprintf("liberties: %d order 2 liberties:%d order 3:%d order 4:%d\n", worm[pos].liberties, worm[pos].liberties2, worm[pos].liberties3, worm[pos].liberties4); /* List all attack points. */ if (worm[pos].attack_points[0] == NO_MOVE) gprintf("no attack point, "); else { gprintf("attack point(s):"); i = 0; while (worm[pos].attack_points[i] != NO_MOVE) { if (i > 0) gprintf(","); gprintf(" %1m: %s", worm[pos].attack_points[i], result_to_string(worm[pos].attack_codes[i])); i++; } gprintf("\n;"); } /* List all defense points. */ if (worm[pos].defense_points[0] == NO_MOVE) gprintf("no defense point, "); else { gprintf("defense point(s):"); i = 0; while (worm[pos].defense_points[i] != NO_MOVE) { if (i > 0) gprintf(","); gprintf(" %1m: %s", worm[pos].defense_points[i], result_to_string(worm[pos].defense_codes[i])); i++; } gprintf("\n;"); } /* List all attack threat points. */ if (worm[pos].attack_threat_points[0] == NO_MOVE) gprintf("no attack threat point, "); else { gprintf("attack threat point(s):"); i = 0; while (worm[pos].attack_threat_points[i] != NO_MOVE) { if (i > 0) gprintf(","); gprintf(" %1m: %s", worm[pos].attack_threat_points[i], result_to_string(worm[pos].attack_threat_codes[i])); i++; } gprintf("\n;"); } /* List all defense threat points. */ if (worm[pos].defense_threat_points[0] == NO_MOVE) gprintf("no defense threat point, "); else { gprintf("defense threat point(s):"); i = 0; while (worm[pos].defense_threat_points[i] != NO_MOVE) { if (i > 0) gprintf(","); gprintf(" %1m: %s", worm[pos].defense_threat_points[i], result_to_string(worm[pos].defense_threat_codes[i])); i++; } gprintf("\n;"); } /* Report lunch if any. */ if (worm[pos].lunch != NO_MOVE) gprintf("lunch at %1m\n", worm[pos].lunch); gprintf("cutstone: %d, cutstone2: %d\n", worm[pos].cutstone, worm[pos].cutstone2); gprintf("genus: %d, ", worm[pos].genus); if (worm[pos].inessential) gprintf("inessential: YES, "); else gprintf("inessential: NO, "); if (worm[pos].invincible) gprintf("invincible: YES, \n"); else gprintf("invincible: NO, \n"); gprintf("unconditional status %s\n", status_to_string(worm[pos].unconditional_status)); } /* * Local Variables: * tab-width: 8 * c-basic-offset: 2 * End: */