4044 lines
118 KiB
C
4044 lines
118 KiB
C
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
|
* 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
#include "liberty.h"
|
|
#include "gg_utils.h"
|
|
#include "move_reasons.h"
|
|
|
|
|
|
/* Count how many distinct strings are (solidly) connected by the move
|
|
* at (pos). Add a bonus for strings with few liberties. Also add
|
|
* bonus for opponent strings put in atari or removed and for own
|
|
* strings in atari adjacent to removed opponent strings.
|
|
*
|
|
* The parameter to_move should be set when color is the color to
|
|
* move. (This function is called for both colors.)
|
|
*/
|
|
static int
|
|
move_connects_strings(int pos, int color, int to_move)
|
|
{
|
|
int ss[4];
|
|
int strings = 0;
|
|
int own_strings = 0;
|
|
int k, l;
|
|
int fewlibs = 0;
|
|
|
|
for (k = 0; k < 4; k++) {
|
|
int ii = pos + delta[k];
|
|
int origin;
|
|
|
|
if (!ON_BOARD(ii) || board[ii] == EMPTY)
|
|
continue;
|
|
|
|
origin = find_origin(ii);
|
|
|
|
for (l = 0; l < strings; l++)
|
|
if (ss[l] == origin)
|
|
break;
|
|
|
|
if (l == strings) {
|
|
ss[strings] = origin;
|
|
strings++;
|
|
}
|
|
}
|
|
|
|
for (k = 0; k < strings; k++) {
|
|
if (worm[ss[k]].invincible)
|
|
continue;
|
|
if (board[ss[k]] == color) {
|
|
int newlibs = approxlib(pos, color, MAXLIBS, NULL);
|
|
own_strings++;
|
|
if (newlibs >= countlib(ss[k])) {
|
|
if (countlib(ss[k]) <= 4)
|
|
fewlibs++;
|
|
if (countlib(ss[k]) <= 2)
|
|
fewlibs++;
|
|
}
|
|
}
|
|
else {
|
|
if (countlib(ss[k]) <= 2)
|
|
fewlibs++;
|
|
if (countlib(ss[k]) <= 1 && to_move) {
|
|
int dummy[MAXCHAIN];
|
|
fewlibs++;
|
|
fewlibs += chainlinks2(ss[k], dummy, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Do some thresholding. */
|
|
if (fewlibs > 4)
|
|
fewlibs = 4;
|
|
if (to_move && is_ko(pos, color, NULL) && fewlibs > 1)
|
|
fewlibs = 1;
|
|
if (fewlibs == 0 && own_strings == 1)
|
|
own_strings = 0;
|
|
|
|
return own_strings + fewlibs;
|
|
}
|
|
|
|
/* Find saved dragons and worms, then call blunder_size(). */
|
|
static float
|
|
value_moves_get_blunder_size(int move, int color)
|
|
{
|
|
signed char saved_dragons[BOARDMAX];
|
|
signed char saved_worms[BOARDMAX];
|
|
signed char safe_stones[BOARDMAX];
|
|
|
|
get_saved_dragons(move, saved_dragons);
|
|
get_saved_worms(move, saved_worms);
|
|
|
|
mark_safe_stones(color, move, saved_dragons, saved_worms, safe_stones);
|
|
|
|
return blunder_size(move, color, NULL, safe_stones);
|
|
}
|
|
|
|
static int
|
|
value_moves_confirm_safety(int move, int color)
|
|
{
|
|
return (value_moves_get_blunder_size(move, color) == 0.0);
|
|
}
|
|
|
|
|
|
/* Test all moves which defend, attack, connect or cut to see if they
|
|
* also attack or defend some other worm.
|
|
*
|
|
* FIXME: We would like to see whether an arbitrary move works to cut
|
|
* or connect something else too.
|
|
*/
|
|
|
|
static void
|
|
find_more_attack_and_defense_moves(int color)
|
|
{
|
|
int unstable_worms[MAX_WORMS];
|
|
int N = 0; /* number of unstable worms */
|
|
int ii;
|
|
int k;
|
|
int other = OTHER_COLOR(color);
|
|
int cursor_at_start_of_line;
|
|
|
|
TRACE("\nLooking for additional attack and defense moves. Trying moves ...\n");
|
|
|
|
/* Identify the unstable worms and store them in a list. */
|
|
for (ii = BOARDMIN; ii < BOARDMAX; ii++) {
|
|
if (IS_STONE(board[ii])
|
|
&& worm[ii].origin == ii
|
|
&& worm[ii].attack_codes[0] != 0
|
|
&& worm[ii].defense_codes[0] != 0) {
|
|
unstable_worms[N] = ii;
|
|
N++;
|
|
}
|
|
}
|
|
|
|
/* To avoid horizon effects, we temporarily increase the depth values. */
|
|
increase_depth_values();
|
|
|
|
for (ii = BOARDMIN; ii < BOARDMAX; ii++) {
|
|
if (board[ii] != EMPTY)
|
|
continue;
|
|
|
|
/* Don't consider send-two-return-one moves here. */
|
|
if (send_two_return_one(ii, color))
|
|
continue;
|
|
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[ii].reason[k];
|
|
|
|
if (r < 0)
|
|
break;
|
|
|
|
if (move_reasons[r].type == ATTACK_MOVE
|
|
|| move_reasons[r].type == ATTACK_MOVE_GOOD_KO
|
|
|| move_reasons[r].type == ATTACK_MOVE_BAD_KO
|
|
|| move_reasons[r].type == DEFEND_MOVE
|
|
|| move_reasons[r].type == DEFEND_MOVE_GOOD_KO
|
|
|| move_reasons[r].type == DEFEND_MOVE_BAD_KO
|
|
|| move_reasons[r].type == CONNECT_MOVE
|
|
|| move_reasons[r].type == CUT_MOVE)
|
|
break;
|
|
/* FIXME: Add code for EITHER_MOVE and ALL_MOVE here. */
|
|
}
|
|
|
|
if (k == MAX_REASONS || move[ii].reason[k] == -1)
|
|
continue;
|
|
|
|
/* Try the move at (ii) and see what happens. */
|
|
cursor_at_start_of_line = 0;
|
|
TRACE("%1m ", ii);
|
|
if (trymove(ii, color, "find_more_attack_and_defense_moves", NO_MOVE)) {
|
|
for (k = 0; k < N; k++) {
|
|
int aa = unstable_worms[k];
|
|
|
|
/* string of our color, see if there still is an attack,
|
|
* unless we already know the move works as defense move.
|
|
*/
|
|
if (board[aa] == color
|
|
&& !defense_move_reason_known(ii, unstable_worms[k])) {
|
|
int acode = attack(aa, NULL);
|
|
if (acode < worm[aa].attack_codes[0]) {
|
|
/* Maybe attack() doesn't find the attack. Try to
|
|
* attack with the stored attack move.
|
|
*/
|
|
int defense_works = 1;
|
|
|
|
if (trymove(worm[aa].attack_points[0], other,
|
|
"find_more_attack_and_defense_moves", 0)) {
|
|
if (!board[aa])
|
|
defense_works = 0;
|
|
else {
|
|
int this_acode = REVERSE_RESULT(find_defense(aa, NULL));
|
|
if (this_acode > acode) {
|
|
acode = this_acode;
|
|
if (acode >= worm[aa].attack_codes[0])
|
|
defense_works = 0;
|
|
}
|
|
}
|
|
popgo();
|
|
}
|
|
|
|
if (defense_works) {
|
|
if (!cursor_at_start_of_line)
|
|
TRACE("\n");
|
|
TRACE("%ofound extra point of defense of %1m at %1m code %d\n",
|
|
aa, ii, REVERSE_RESULT(acode));
|
|
cursor_at_start_of_line = 1;
|
|
add_defense_move(ii, aa, REVERSE_RESULT(acode));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* string of opponent color, see if there still is a defense,
|
|
* unless we already know the move works as attack move.
|
|
*/
|
|
if (board[aa] == other
|
|
&& !attack_move_reason_known(ii, unstable_worms[k])) {
|
|
|
|
int dcode = find_defense(aa, NULL);
|
|
if (dcode < worm[aa].defense_codes[0]) {
|
|
/* Maybe find_defense() doesn't find the defense. Try to
|
|
* defend with the stored defense move.
|
|
*
|
|
* Another option is maybe there is no attack anymore
|
|
* (e.g. we pushed the worm into seki), find_defense()
|
|
* could easily fail in that case.
|
|
*/
|
|
int attack_works = 1;
|
|
|
|
if (attack(aa, NULL) >= worm[aa].attack_codes[0]) {
|
|
if (trymove(worm[aa].defense_points[0], other,
|
|
"find_more_attack_and_defense_moves", 0)) {
|
|
int this_dcode = REVERSE_RESULT(attack(aa, NULL));
|
|
if (this_dcode > dcode) {
|
|
dcode = this_dcode;
|
|
if (dcode >= worm[aa].defense_codes[0])
|
|
attack_works = 0;
|
|
}
|
|
popgo();
|
|
}
|
|
}
|
|
else
|
|
attack_works = 0;
|
|
|
|
if (attack_works) {
|
|
if (!cursor_at_start_of_line)
|
|
TRACE("\n");
|
|
TRACE("%ofound extra point of attack of %1m at %1m code %d\n",
|
|
aa, ii, REVERSE_RESULT(dcode));
|
|
cursor_at_start_of_line = 1;
|
|
add_attack_move(ii, aa, REVERSE_RESULT(dcode));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
popgo();
|
|
}
|
|
}
|
|
|
|
TRACE("\n");
|
|
decrease_depth_values();
|
|
}
|
|
|
|
|
|
/* Do the real job of find_more_owl_attack_and_defense_moves() with given
|
|
* move reason at given position and for given target (`what'). This
|
|
* function is used from induce_secondary_move_reasons() for upgrading
|
|
* one specific move reason only.
|
|
*/
|
|
static void
|
|
do_find_more_owl_attack_and_defense_moves(int color, int pos,
|
|
int move_reason_type, int what)
|
|
{
|
|
int k;
|
|
int dd1 = NO_MOVE;
|
|
int dd2 = NO_MOVE;
|
|
int save_verbose;
|
|
|
|
gg_assert(stackp == 0);
|
|
|
|
/* Never consider moves of the send-two-return-one type here. */
|
|
if (send_two_return_one(pos, color))
|
|
return;
|
|
|
|
/* Never consider moves playing into snapback here. */
|
|
if (playing_into_snapback(pos, color))
|
|
return;
|
|
|
|
save_verbose = verbose;
|
|
if (verbose > 0)
|
|
verbose --;
|
|
|
|
if (move_reason_type == STRATEGIC_ATTACK_MOVE
|
|
|| move_reason_type == STRATEGIC_DEFEND_MOVE)
|
|
dd1 = what;
|
|
else if (move_reason_type == ATTACK_MOVE
|
|
|| move_reason_type == ATTACK_MOVE_GOOD_KO
|
|
|| move_reason_type == ATTACK_MOVE_BAD_KO
|
|
|| move_reason_type == DEFEND_MOVE
|
|
|| move_reason_type == DEFEND_MOVE_GOOD_KO
|
|
|| move_reason_type == DEFEND_MOVE_BAD_KO
|
|
|| move_reason_type == VITAL_EYE_MOVE)
|
|
dd1 = what;
|
|
else if (move_reason_type == CONNECT_MOVE) {
|
|
int worm1 = conn_worm1[what];
|
|
int worm2 = conn_worm2[what];
|
|
|
|
dd1 = dragon[worm1].origin;
|
|
dd2 = dragon[worm2].origin;
|
|
if (dd1 == dd2)
|
|
dd2 = NO_MOVE;
|
|
}
|
|
else {
|
|
verbose = save_verbose;
|
|
return;
|
|
}
|
|
|
|
for (k = 0; k < 2; k++) {
|
|
int dd = (k == 0 ? dd1 : dd2);
|
|
|
|
if (dd == NO_MOVE)
|
|
continue;
|
|
|
|
/* Don't care about inessential dragons. */
|
|
if (DRAGON2(dd).safety == INESSENTIAL)
|
|
continue;
|
|
|
|
if (DRAGON2(dd).owl_status != CRITICAL)
|
|
continue;
|
|
|
|
if ((move_reason_type == STRATEGIC_ATTACK_MOVE
|
|
|| move_reason_type == ATTACK_MOVE
|
|
|| move_reason_type == ATTACK_MOVE_GOOD_KO
|
|
|| move_reason_type == ATTACK_MOVE_BAD_KO
|
|
|| (move_reason_type == VITAL_EYE_MOVE
|
|
&& board[dd] == OTHER_COLOR(color)))
|
|
&& !owl_attack_move_reason_known(pos, dd)) {
|
|
int kworm = NO_MOVE;
|
|
int acode = owl_does_attack(pos, dd, &kworm);
|
|
|
|
if (acode >= DRAGON2(dd).owl_attack_code) {
|
|
add_owl_attack_move(pos, dd, kworm, acode);
|
|
if (save_verbose)
|
|
gprintf("Move at %1m upgraded to owl attack on %1m (%s).\n",
|
|
pos, dd, result_to_string(acode));
|
|
}
|
|
}
|
|
|
|
if ((move_reason_type == STRATEGIC_DEFEND_MOVE
|
|
|| move_reason_type == CONNECT_MOVE
|
|
|| move_reason_type == DEFEND_MOVE
|
|
|| move_reason_type == DEFEND_MOVE_GOOD_KO
|
|
|| move_reason_type == DEFEND_MOVE_BAD_KO
|
|
|| (move_reason_type == VITAL_EYE_MOVE
|
|
&& board[dd] == color))
|
|
&& !owl_defense_move_reason_known(pos, dd)) {
|
|
int kworm = NO_MOVE;
|
|
/* FIXME: Better use owl_connection_defend() for CONNECT_MOVE ? */
|
|
int dcode = owl_does_defend(pos, dd, &kworm);
|
|
|
|
if (dcode >= DRAGON2(dd).owl_defense_code) {
|
|
if (dcode == LOSS)
|
|
add_loss_move(pos, dd, kworm);
|
|
else
|
|
add_owl_defense_move(pos, dd, dcode);
|
|
if (save_verbose)
|
|
gprintf("Move at %1m upgraded to owl defense for %1m (%s).\n",
|
|
pos, dd, result_to_string(dcode));
|
|
}
|
|
}
|
|
}
|
|
|
|
verbose = save_verbose;
|
|
}
|
|
|
|
|
|
|
|
/* Try whether the move at (pos) for (color) is also an owl attack on
|
|
* (target). (dist) is the distance to the dragon, and is used for a
|
|
* safety heuristic: distant moves are only accepted if they kill within
|
|
* few owl nodes.
|
|
*/
|
|
static void
|
|
try_large_scale_owl_attack(int pos, int color, int target, int dist)
|
|
{
|
|
int owl_nodes_before;
|
|
int owl_nodes_used;
|
|
int kworm = NO_MOVE;
|
|
int acode;
|
|
int save_verbose = verbose;
|
|
int save_owl_node_limit = owl_node_limit;
|
|
|
|
ASSERT1(board[target] == OTHER_COLOR(color), pos);
|
|
ASSERT1(!owl_attack_move_reason_known(pos, target), pos);
|
|
DEBUG(DEBUG_LARGE_SCALE, "Trying large scale move %1m on %1m\n", pos, target);
|
|
|
|
/* To avoid horizon effects, we temporarily increase
|
|
* the depth values to find the large scale attacks.
|
|
*/
|
|
increase_depth_values();
|
|
|
|
/* To reduce the amount of aji allowed for large scale
|
|
* attacks, we reduce the owl limit to 350 nodes for
|
|
* attacks at distance <= 1, and 150 nodes for attacks at
|
|
* distance >= 2.
|
|
*/
|
|
if (dist <= 1)
|
|
owl_node_limit *= 0.35;
|
|
else
|
|
owl_node_limit *= 0.15;
|
|
|
|
if (DRAGON2(target).owl_attack_node_count < owl_node_limit) {
|
|
if (verbose > 0)
|
|
verbose--;
|
|
|
|
owl_nodes_before = get_owl_node_counter();
|
|
acode = owl_does_attack(pos, target, &kworm);
|
|
owl_nodes_used = get_owl_node_counter() - owl_nodes_before;
|
|
|
|
if (acode >= DRAGON2(target).owl_attack_code
|
|
&& acode == WIN) {
|
|
add_owl_attack_move(pos, target, kworm, acode);
|
|
DEBUG(DEBUG_LARGE_SCALE | DEBUG_MOVE_REASONS,
|
|
"Move at %1m owl-attacks %1m on a large scale(%s).\n",
|
|
pos, target, result_to_string(acode));
|
|
}
|
|
else
|
|
DEBUG(DEBUG_LARGE_SCALE,
|
|
"Move at %1m isn't a clean large scale attack on %1m (%s).\n",
|
|
pos, target, result_to_string(acode));
|
|
|
|
DEBUG(DEBUG_LARGE_SCALE, " owl nodes used = %d, dist = %d\n",
|
|
owl_nodes_used, dist);
|
|
/* Restore settings. */
|
|
verbose = save_verbose;
|
|
}
|
|
decrease_depth_values();
|
|
owl_node_limit = save_owl_node_limit;
|
|
}
|
|
|
|
|
|
#define MAXIMUM_LARGE_SCALE_DIST 3
|
|
|
|
/* Test all the moves to see whether they can owl-attack a specific
|
|
* dragon on a large scale . Tested moves are
|
|
* 1. Moves that already have a move reason.
|
|
* 2. Are not too far away.
|
|
* The distance used is the Manhattan distance, and the maximum
|
|
* distance is MAXIMUM_LARGE_SCALE_DIST.
|
|
*/
|
|
static void
|
|
find_large_scale_owl_attacks_on_dragon(int color, int target)
|
|
{
|
|
int x, y;
|
|
int x_min = board_size;
|
|
int x_max = 0;
|
|
int y_min = board_size;
|
|
int y_max = 0;
|
|
int dist;
|
|
|
|
ASSERT1(board[target] == OTHER_COLOR(color), target);
|
|
|
|
/* Find the physical extension of the dragon. */
|
|
for (x = 0; x < board_size; x++)
|
|
for (y = 0; y < board_size; y++) {
|
|
if (is_same_dragon(target, POS(x, y))) {
|
|
if (x < x_min)
|
|
x_min = x;
|
|
if (x > x_max)
|
|
x_max = x;
|
|
if (y < y_min)
|
|
y_min = y;
|
|
if (y > y_max)
|
|
y_max = y;
|
|
}
|
|
}
|
|
ASSERT1(x_min <= x_max && y_min <= y_max, target);
|
|
|
|
/* Try to find large scale attacks.
|
|
* We do this by first trying to find attacks at dist = 0, then
|
|
* dist = 1, etc., up to MAXIMUM_LARGE_SCALE_DIST.
|
|
*/
|
|
for (dist = 0; dist <= MAXIMUM_LARGE_SCALE_DIST; dist++)
|
|
for (x = gg_max(x_min - dist, 0);
|
|
x <= gg_min(x_max + dist, board_size - 1); x++)
|
|
for (y = gg_max(y_min - dist, 0);
|
|
y <= gg_min(y_max + dist, board_size - 1); y++) {
|
|
int pos = POS(x, y);
|
|
ASSERT1(ON_BOARD2(x, y), pos);
|
|
|
|
if (board[pos] == EMPTY) {
|
|
int a, b, dx, dy;
|
|
a = abs(x - x_min);
|
|
b = abs(x - x_max);
|
|
dx = gg_min(a, b);
|
|
a = abs(y - y_min);
|
|
b = abs(y - y_max);
|
|
dy = gg_min(a, b);
|
|
|
|
if (gg_max(dx, dy) == dist
|
|
&& move[pos].reason[0] >= 0
|
|
&& !owl_attack_move_reason_known(pos, target))
|
|
/* Maximum Manhatan distance, move reason known but no owl
|
|
* attack yet.
|
|
*/
|
|
try_large_scale_owl_attack(pos, color, target, dist);
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Try large scale owl attacks against all enemy dragons that are
|
|
* small (size <= 6) and critical.
|
|
*/
|
|
static void
|
|
find_large_scale_owl_attack_moves(int color)
|
|
{
|
|
int d;
|
|
|
|
DEBUG(DEBUG_LARGE_SCALE, "\nTrying to find large scale attack moves.\n");
|
|
for (d = 0; d < number_of_dragons; d++) {
|
|
int target = dragon2[d].origin;
|
|
if (dragon[target].color == OTHER_COLOR(color)
|
|
&& dragon[target].size <= 6
|
|
&& dragon[target].status == CRITICAL
|
|
&& dragon2[d].owl_status == CRITICAL) {
|
|
DEBUG(DEBUG_LARGE_SCALE, "Small critical dragon found at %1m\n", target);
|
|
find_large_scale_owl_attacks_on_dragon(color, target);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Test certain moves to see whether they (too) can owl-attack or
|
|
* defend an owl critical dragon. Tested moves are
|
|
* 1. Strategical attacks or defenses for the dragon.
|
|
* 2. Vital eye points for the dragon.
|
|
* 3. Tactical attacks or defenses for a part of the dragon.
|
|
* 4. Moves connecting the dragon to something else.
|
|
*/
|
|
static void
|
|
find_more_owl_attack_and_defense_moves(int color)
|
|
{
|
|
int pos, pos2;
|
|
int k;
|
|
int dd = NO_MOVE;
|
|
int worth_trying;
|
|
int save_verbose;
|
|
struct eye_data *our_eyes;
|
|
struct eye_data *your_eyes;
|
|
struct vital_eye_points *our_vital_points;
|
|
struct vital_eye_points *your_vital_points;
|
|
|
|
if (verbose)
|
|
gprintf("\nTrying to upgrade strategical attack and defense moves.\n");
|
|
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
if (!ON_BOARD(pos))
|
|
continue;
|
|
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos].reason[k];
|
|
if (r < 0)
|
|
break;
|
|
|
|
do_find_more_owl_attack_and_defense_moves(color, pos,
|
|
move_reasons[r].type,
|
|
move_reasons[r].what);
|
|
}
|
|
}
|
|
|
|
if (verbose)
|
|
gprintf("\nTrying vital eye moves as owl attacks.\n");
|
|
if (color == WHITE) {
|
|
our_eyes = white_eye;
|
|
your_eyes = black_eye;
|
|
our_vital_points = white_vital_points;
|
|
your_vital_points = black_vital_points;
|
|
}
|
|
else {
|
|
our_eyes = black_eye;
|
|
your_eyes = white_eye;
|
|
our_vital_points = black_vital_points;
|
|
your_vital_points = white_vital_points;
|
|
}
|
|
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
if (!ON_BOARD(pos))
|
|
continue;
|
|
if (our_eyes[pos].origin == pos
|
|
&& our_vital_points[pos].defense_points[0] != NO_MOVE) {
|
|
int k, dr;
|
|
find_eye_dragons(pos, our_eyes, color, &dr, 1);
|
|
for (k = 0; k < MAX_EYE_ATTACKS; k++) {
|
|
int move = our_vital_points[pos].defense_points[k];
|
|
if (move == NO_MOVE)
|
|
break;
|
|
do_find_more_owl_attack_and_defense_moves(color, move,
|
|
VITAL_EYE_MOVE, dr);
|
|
}
|
|
}
|
|
if (your_eyes[pos].origin == pos
|
|
&& your_vital_points[pos].attack_points[0] != NO_MOVE) {
|
|
int k, dr;
|
|
find_eye_dragons(pos, your_eyes, OTHER_COLOR(color), &dr, 1);
|
|
for (k = 0; k < MAX_EYE_ATTACKS; k++) {
|
|
int move = your_vital_points[pos].attack_points[k];
|
|
if (move == NO_MOVE)
|
|
break;
|
|
do_find_more_owl_attack_and_defense_moves(color, move,
|
|
VITAL_EYE_MOVE, dr);
|
|
}
|
|
}
|
|
}
|
|
|
|
save_verbose = verbose;
|
|
if (verbose > 0)
|
|
verbose--;
|
|
|
|
/* If two critical dragons are adjacent, test whether a move to owl
|
|
* attack or defend one also is effective on the other.
|
|
*/
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
if (IS_STONE(board[pos])
|
|
&& dragon[pos].origin == pos
|
|
&& DRAGON2(pos).owl_status == CRITICAL) {
|
|
for (pos2 = BOARDMIN; pos2 < BOARDMAX; pos2++) {
|
|
if (board[pos2] != EMPTY)
|
|
continue;
|
|
worth_trying = 0;
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos2].reason[k];
|
|
|
|
if (r < 0)
|
|
break;
|
|
if (move_reasons[r].type == OWL_ATTACK_MOVE
|
|
|| move_reasons[r].type == OWL_ATTACK_MOVE_GOOD_KO
|
|
|| move_reasons[r].type == OWL_ATTACK_MOVE_BAD_KO
|
|
|| move_reasons[r].type == OWL_DEFEND_MOVE
|
|
|| move_reasons[r].type == OWL_DEFEND_MOVE_GOOD_KO
|
|
|| move_reasons[r].type == OWL_DEFEND_MOVE_BAD_KO) {
|
|
dd = move_reasons[r].what;
|
|
if (are_neighbor_dragons(dd, pos)) {
|
|
worth_trying = 1;
|
|
break;
|
|
}
|
|
}
|
|
/* else ...
|
|
FIXME: what about the new OWL_ATTACK_MOVE_GAIN codes ?
|
|
*/
|
|
}
|
|
|
|
if (worth_trying) {
|
|
if (board[pos] == color
|
|
&& !owl_defense_move_reason_known(pos2, pos)) {
|
|
int kworm = NO_MOVE;
|
|
int dcode = owl_does_defend(pos2, pos, &kworm);
|
|
if (dcode >= DRAGON2(pos).owl_defense_code) {
|
|
if (dcode == LOSS)
|
|
add_loss_move(pos2, pos, kworm);
|
|
else
|
|
add_owl_defense_move(pos2, pos, dcode);
|
|
if (save_verbose)
|
|
gprintf("Move at %1m also owl defends %1m (%s).\n",
|
|
pos2, pos, result_to_string(dcode));
|
|
}
|
|
|
|
}
|
|
else if (board[pos] != color
|
|
&& !owl_attack_move_reason_known(pos2, pos)) {
|
|
int kworm = NO_MOVE;
|
|
int acode = owl_does_attack(pos2, pos, &kworm);
|
|
if (acode >= DRAGON2(pos).owl_attack_code) {
|
|
add_owl_attack_move(pos2, pos, kworm, acode);
|
|
if (save_verbose)
|
|
gprintf("Move at %1m also owl attacks %1m (%s).\n",
|
|
pos2, pos, result_to_string(acode));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
verbose = save_verbose;
|
|
}
|
|
|
|
/* Tests whether the potential semeai move at (pos) with details given via
|
|
* (*reason) works, and adds a semeai move if applicable.
|
|
*/
|
|
static void
|
|
try_potential_semeai_move(int pos, int color, struct move_reason *reason)
|
|
{
|
|
int dr1 = semeai_target1[reason->what];
|
|
int dr2 = semeai_target2[reason->what];
|
|
int resulta, resultb, certain, old_certain;
|
|
ASSERT1(IS_STONE(board[dr1]), pos);
|
|
switch (reason->type) {
|
|
case POTENTIAL_SEMEAI_ATTACK:
|
|
owl_analyze_semeai_after_move(pos, color, dr1, dr2,
|
|
&resulta, &resultb, NULL,
|
|
1, &certain, 0);
|
|
old_certain = DRAGON2(dr1).semeai_attack_certain;
|
|
break;
|
|
case POTENTIAL_SEMEAI_DEFENSE:
|
|
old_certain = DRAGON2(dr1).semeai_defense_certain;
|
|
/* In this case other dragon gets to move first after forced move. */
|
|
owl_analyze_semeai_after_move(pos, color, dr2, dr1,
|
|
&resulta, &resultb, NULL,
|
|
1, &certain, 0);
|
|
break;
|
|
default:
|
|
ASSERT1(0, pos);
|
|
}
|
|
if (resulta == 0 && resultb == 0
|
|
&& (certain || !old_certain)) {
|
|
add_semeai_move(pos, dr1);
|
|
DEBUG(DEBUG_SEMEAI,
|
|
"Potential semeai move at %1m for dragon at %1m is real\n",
|
|
pos, dr1);
|
|
}
|
|
else
|
|
DEBUG(DEBUG_MOVE_REASONS, "Potential semeai move at %1m for %1m discarded\n",
|
|
pos, dr1);
|
|
}
|
|
|
|
/* This functions tests all potential semeai attack moves whether they work,
|
|
* provided that there is at least one other move reasons stored for the
|
|
* relevant position.
|
|
*/
|
|
static void
|
|
find_more_semeai_moves(int color)
|
|
{
|
|
int pos;
|
|
int save_verbose = verbose;
|
|
|
|
if (verbose > 0)
|
|
verbose--;
|
|
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
int k, r;
|
|
int potential_semeai_move_found = 0;
|
|
int other_move_reason_found = 0;
|
|
|
|
if (!ON_BOARD1(pos))
|
|
continue;
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
r = move[pos].reason[k];
|
|
if (r < 0)
|
|
break;
|
|
switch (move_reasons[r].type) {
|
|
case POTENTIAL_SEMEAI_ATTACK:
|
|
case POTENTIAL_SEMEAI_DEFENSE:
|
|
potential_semeai_move_found = 1;
|
|
break;
|
|
default:
|
|
other_move_reason_found = 1;
|
|
}
|
|
}
|
|
if ((r < 0 || k == MAX_REASONS)
|
|
&& !other_move_reason_found)
|
|
continue;
|
|
if (!potential_semeai_move_found)
|
|
continue;
|
|
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos].reason[k];
|
|
if (r < 0)
|
|
break;
|
|
if (move_reasons[r].type == POTENTIAL_SEMEAI_ATTACK
|
|
|| move_reasons[r].type == POTENTIAL_SEMEAI_DEFENSE)
|
|
try_potential_semeai_move(pos, color, &(move_reasons[r]));
|
|
}
|
|
}
|
|
verbose = save_verbose;
|
|
}
|
|
|
|
|
|
/*
|
|
* Any move that captures or defends a worm also potentially connects
|
|
* or cuts the surrounding strings. Find these secondary move reasons
|
|
* and verify them by connection reading.
|
|
*
|
|
* We also let an owl attack count as a strategical defense of our
|
|
* neighbors of the owl attacked dragon. We only do this for
|
|
* tactically safe dragons, however, because otherwise the effects of
|
|
* capturing have already been taken into account elsewhere.
|
|
*
|
|
* Also, connecting moves played on inhibited points possibly remove
|
|
* nearby connection inhibitions like in following example :
|
|
*
|
|
* .OX. The * move connects _all_ O stones together, not only
|
|
* O... the 2 lower ones.
|
|
* XO*O
|
|
* X.X.
|
|
*
|
|
*/
|
|
|
|
static void
|
|
induce_secondary_move_reasons(int color)
|
|
{
|
|
int pos;
|
|
int k;
|
|
int i, j;
|
|
int aa;
|
|
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
if (!ON_BOARD(pos))
|
|
continue;
|
|
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos].reason[k];
|
|
|
|
if (r < 0)
|
|
break;
|
|
|
|
if (move_reasons[r].type == ATTACK_MOVE
|
|
|| move_reasons[r].type == DEFEND_MOVE) {
|
|
int attack_move;
|
|
int color_to_move;
|
|
int num_adj, adjs[MAXCHAIN];
|
|
|
|
aa = move_reasons[r].what;
|
|
|
|
if (move_reasons[r].type == ATTACK_MOVE) {
|
|
attack_move = 1;
|
|
color_to_move = OTHER_COLOR(board[aa]);
|
|
}
|
|
else {
|
|
attack_move = 0;
|
|
color_to_move = board[aa];
|
|
}
|
|
|
|
if (worm[aa].defense_codes[0] == 0)
|
|
continue; /* No defense. */
|
|
|
|
/* Don't care about inessential dragons. */
|
|
if (DRAGON2(aa).safety == INESSENTIAL)
|
|
continue;
|
|
|
|
/*
|
|
* If this is a defense move and the defense is futile for
|
|
* strategical reasons, we shouldn't induce a cutting move
|
|
* reason.
|
|
*
|
|
* FIXME: We may want to revise this policy.
|
|
*/
|
|
if (!attack_move && !move[pos].move_safety)
|
|
continue;
|
|
|
|
num_adj = extended_chainlinks(aa, adjs, 1);
|
|
|
|
for (i = 0; i < num_adj; i++) {
|
|
for (j = i+1; j < num_adj; j++) {
|
|
int adj1 = adjs[i];
|
|
int adj2 = adjs[j];
|
|
|
|
if (board[adj1] != board[adj2])
|
|
continue;
|
|
if (attack_move
|
|
&& board[adj1] != board[aa]
|
|
&& !disconnect(adj1, adj2, NULL))
|
|
continue;
|
|
if (!attack_move
|
|
&& board[adj1] != board[aa]
|
|
&& !string_connect(adj1, adj2, NULL))
|
|
continue;
|
|
if (attack_move
|
|
&& board[adj1] == board[aa])
|
|
continue;
|
|
if (!attack_move
|
|
&& board[adj1] == board[aa]
|
|
&& !disconnect(adj1, adj2, NULL))
|
|
continue;
|
|
|
|
if (trymove(pos, color_to_move, "induce_secondary_move_reasons",
|
|
aa)) {
|
|
if (attack_move
|
|
&& board[adj1] != board[aa]
|
|
&& !disconnect(adj1, adj2, NULL)) {
|
|
popgo();
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
"Connection move at %1m induced for %1m/%1m due to attack of %1m\n",
|
|
pos, adj1, adj2, aa);
|
|
add_connection_move(pos, adj1, adj2);
|
|
do_find_more_owl_attack_and_defense_moves(color, pos, CONNECT_MOVE,
|
|
find_connection(adj1, adj2));
|
|
}
|
|
else if (!attack_move
|
|
&& board[adj1] != board[aa]
|
|
&& !string_connect(adj1, adj2, NULL)) {
|
|
popgo();
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
"Cut move at %1m induced for %1m/%1m due to defense of %1m\n",
|
|
pos, adj1, adj2, aa);
|
|
add_cut_move(pos, adj1, adj2);
|
|
}
|
|
else if (!attack_move
|
|
&& board[adj1] == board[aa]
|
|
&& !disconnect(adj1, adj2, NULL)) {
|
|
popgo();
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
"Connection move at %1m induced for %1m/%1m due to defense of %1m\n",
|
|
pos, adj1, adj2, aa);
|
|
add_connection_move(pos, adj1, adj2);
|
|
do_find_more_owl_attack_and_defense_moves(color, pos, CONNECT_MOVE,
|
|
find_connection(adj1, adj2));
|
|
}
|
|
else
|
|
popgo();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Strategical attack move reason is induced for moves that
|
|
* defend neighbor strings of weak opponent dragons a. We
|
|
* only count strings that are large (more than three stones)
|
|
* or adjoin at least two non-dead non-single-stone opponent
|
|
* dragons.
|
|
*/
|
|
if (!attack_move) {
|
|
int strategically_valuable = (worm[aa].size > 3);
|
|
signed char neighbor_dragons[BOARDMAX];
|
|
|
|
memset(neighbor_dragons, 0, sizeof(neighbor_dragons));
|
|
|
|
if (!strategically_valuable) {
|
|
int num_dragons = 0;
|
|
|
|
for (i = 0; i < num_adj; i++) {
|
|
int origin = dragon[adjs[i]].origin;
|
|
|
|
if (board[origin] != color_to_move
|
|
&& neighbor_dragons[origin] != 1
|
|
&& dragon[origin].size > 1
|
|
&& dragon[origin].status != DEAD) {
|
|
if (++num_dragons == 2) {
|
|
strategically_valuable = 1;
|
|
break;
|
|
}
|
|
|
|
neighbor_dragons[origin] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (strategically_valuable) {
|
|
for (i = 0; i < num_adj; i++) {
|
|
int origin = dragon[adjs[i]].origin;
|
|
|
|
if (board[origin] != color_to_move
|
|
&& neighbor_dragons[origin] != 2
|
|
&& dragon[origin].status != DEAD
|
|
&& dragon_weak(origin)) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
"Strategical attack move at %1m induced for %1m due to defense of %1m\n",
|
|
pos, origin, aa);
|
|
add_strategical_attack_move(pos, origin);
|
|
do_find_more_owl_attack_and_defense_moves(color, pos,
|
|
STRATEGIC_ATTACK_MOVE,
|
|
origin);
|
|
|
|
neighbor_dragons[origin] = 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (move_reasons[r].type == OWL_ATTACK_MOVE) {
|
|
aa = move_reasons[r].what;
|
|
for (i = 0; i < DRAGON2(aa).neighbors; i++) {
|
|
int bb = dragon2[DRAGON2(aa).adjacent[i]].origin;
|
|
if (dragon[bb].color == color && worm[bb].attack_codes[0] == 0
|
|
&& !DRAGON2(bb).semeais) {
|
|
add_strategical_defense_move(pos, bb);
|
|
do_find_more_owl_attack_and_defense_moves(color, pos,
|
|
STRATEGIC_DEFEND_MOVE,
|
|
bb);
|
|
DEBUG(DEBUG_MOVE_REASONS, "Strategic defense at %1m induced for %1m due to owl attack on %1m\n",
|
|
pos, bb, aa);
|
|
}
|
|
}
|
|
}
|
|
else if (move_reasons[r].type == CONNECT_MOVE
|
|
&& cut_possible(pos, OTHER_COLOR(color))) {
|
|
int worm1 = conn_worm1[move_reasons[r].what];
|
|
int worm2 = conn_worm2[move_reasons[r].what];
|
|
int pos2;
|
|
for (pos2 = BOARDMIN; pos2 < BOARDMAX; pos2++) {
|
|
if (ON_BOARD(pos2) && board[pos2] == EMPTY
|
|
&& cut_possible(pos2, OTHER_COLOR(color))
|
|
&& square_dist(pos, pos2) <= 5) {
|
|
for (j = 0; j < 8; j++) {
|
|
int pos3 = pos2 + delta[j];
|
|
if (ON_BOARD(pos3) && board[pos3] == color
|
|
&& !is_same_worm(pos3, worm1)
|
|
&& !is_same_worm(pos3, worm2)) {
|
|
if (trymove(pos, color, "induce_secondary_move_reasons-B",
|
|
worm1)) {
|
|
int break1 = disconnect(pos3, worm1, NULL);
|
|
int break2 = disconnect(pos3, worm2, NULL);
|
|
popgo();
|
|
|
|
if (!break1) {
|
|
add_connection_move(pos, pos3, worm1);
|
|
do_find_more_owl_attack_and_defense_moves(color, pos, CONNECT_MOVE,
|
|
find_connection(pos3, worm1));
|
|
DEBUG(DEBUG_MOVE_REASONS, "Connection at %1m induced for %1m/%1m due to connection at %1m/%1m\n",
|
|
pos, worm1, worm2, pos3, worm1);
|
|
}
|
|
|
|
if (!break2) {
|
|
add_connection_move(pos, pos3, worm2);
|
|
do_find_more_owl_attack_and_defense_moves(color, pos, CONNECT_MOVE,
|
|
find_connection(pos3, worm2));
|
|
DEBUG(DEBUG_MOVE_REASONS, "Connection at %1m induced for %1m/%1m due to connection at %1m/%1m\n",
|
|
pos, worm1, worm2, pos3, worm2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Examine the strategical and tactical safety of the moves. This is
|
|
* used to decide whether or not the stone should generate influence
|
|
* when the move is evaluated. The idea is to avoid overestimating the
|
|
* value of strategically unsafe defense moves and connections of dead
|
|
* dragons. This sets the move.move_safety field.
|
|
*/
|
|
static void
|
|
examine_move_safety(int color)
|
|
{
|
|
int pos;
|
|
int k;
|
|
|
|
start_timer(3);
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
int safety = 0;
|
|
int tactical_safety = 0;
|
|
if (!ON_BOARD(pos))
|
|
continue;
|
|
tactical_safety = is_known_safe_move(pos);
|
|
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos].reason[k];
|
|
int type;
|
|
int what;
|
|
|
|
if (r == -1)
|
|
break;
|
|
type = move_reasons[r].type;
|
|
what = move_reasons[r].what;
|
|
switch (type) {
|
|
case CUT_MOVE:
|
|
/* We don't trust cut moves, unless some other move reason
|
|
* indicates they are safe.
|
|
*/
|
|
break;
|
|
case OWL_DEFEND_MOVE:
|
|
case OWL_DEFEND_MOVE_GOOD_KO:
|
|
case OWL_DEFEND_MOVE_BAD_KO:
|
|
case OWL_DEFEND_MOVE_LOSS:
|
|
{
|
|
int ii;
|
|
for (ii = first_worm_in_dragon(what); ii != NO_MOVE;
|
|
ii = next_worm_in_dragon(ii)) {
|
|
if (!play_connect_n(color, 0, 1, pos, ii, pos))
|
|
break;
|
|
}
|
|
if (ii != NO_MOVE) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
}
|
|
break;
|
|
}
|
|
case SEMEAI_MOVE:
|
|
case MY_ATARI_ATARI_MOVE:
|
|
case YOUR_ATARI_ATARI_MOVE:
|
|
case EITHER_MOVE: /* FIXME: More advanced handling? */
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
break;
|
|
case ALL_MOVE:
|
|
/* We don't trust these, unless some other move reason
|
|
* indicates safety.
|
|
*/
|
|
break;
|
|
case EXPAND_TERRITORY_MOVE:
|
|
case EXPAND_MOYO_MOVE:
|
|
case INVASION_MOVE: /* A real invasion should be safe.
|
|
A sacrifice is something else.*/
|
|
safety = 1;
|
|
break;
|
|
case ATTACK_MOVE:
|
|
case ATTACK_MOVE_GOOD_KO:
|
|
case ATTACK_MOVE_BAD_KO:
|
|
case OWL_ATTACK_MOVE:
|
|
case OWL_ATTACK_MOVE_GOOD_KO:
|
|
case OWL_ATTACK_MOVE_BAD_KO:
|
|
case OWL_ATTACK_MOVE_GAIN:
|
|
{
|
|
int aa = NO_MOVE;
|
|
int bb = NO_MOVE;
|
|
int size;
|
|
int m;
|
|
int our_color_neighbors;
|
|
|
|
if (type == ATTACK_MOVE
|
|
|| type == ATTACK_MOVE_GOOD_KO
|
|
|| type == ATTACK_MOVE_BAD_KO) {
|
|
aa = what;
|
|
size = worm[aa].effective_size;
|
|
}
|
|
else if (type == OWL_ATTACK_MOVE_GAIN) {
|
|
aa = either_data[what].what2;
|
|
size = worm[aa].effective_size;
|
|
}
|
|
else {
|
|
aa = what;
|
|
size = dragon[aa].effective_size;
|
|
}
|
|
|
|
/* No worries if we catch something big. */
|
|
if (size >= 8) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
break;
|
|
}
|
|
|
|
/* If the victim has multiple neighbor dragons of our
|
|
* color, we leave it to the connection move reason to
|
|
* determine safety.
|
|
*
|
|
* The exception is an owl_attack where we only require
|
|
* one neighbor to be alive.
|
|
*/
|
|
our_color_neighbors = 0;
|
|
if (type == ATTACK_MOVE
|
|
|| type == ATTACK_MOVE_GOOD_KO
|
|
|| type == ATTACK_MOVE_BAD_KO) {
|
|
/* We could use the same code as for OWL_ATTACK_MOVE
|
|
* below if we were certain that the capturable string
|
|
* had not been amalgamated with a living dragon.
|
|
*/
|
|
int num_adj, adjs[MAXCHAIN];
|
|
|
|
num_adj = chainlinks(aa, adjs);
|
|
for (m = 0; m < num_adj; m++) {
|
|
int adj = adjs[m];
|
|
|
|
if (board[adj] == color) {
|
|
/* Check whether this string is part of the same
|
|
* dragon as an earlier string. We only want to
|
|
* count distinct neighbor dragons.
|
|
*/
|
|
int n;
|
|
|
|
for (n = 0; n < m; n++)
|
|
if (dragon[adjs[n]].id == dragon[adj].id)
|
|
break;
|
|
if (n == m) {
|
|
/* New dragon. */
|
|
our_color_neighbors++;
|
|
bb = adj;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (m = 0; m < DRAGON2(aa).neighbors; m++)
|
|
if (DRAGON(DRAGON2(aa).adjacent[m]).color == color) {
|
|
our_color_neighbors++;
|
|
bb = dragon2[DRAGON2(aa).adjacent[m]].origin;
|
|
if (dragon[bb].status == ALIVE) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (our_color_neighbors > 1)
|
|
break;
|
|
|
|
/* It may happen in certain positions that no neighbor of
|
|
* our color is found. The working hypothesis is that
|
|
* the move is safe then. One example is a position like
|
|
*
|
|
* ----+
|
|
* OX.X|
|
|
* OOX.|
|
|
* OOX|
|
|
* OO|
|
|
*
|
|
* where the top right stone only has friendly neighbors
|
|
* but can be attacked.
|
|
*
|
|
* As a further improvement, we also look for a friendly
|
|
* dragon adjacent to the considered move.
|
|
*/
|
|
|
|
for (m = 0; m < 4; m++) {
|
|
int d = delta[m];
|
|
if (board[pos+d] == color) {
|
|
bb = pos + d;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bb == NO_MOVE) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
break;
|
|
}
|
|
|
|
/* If the attacker is thought to be alive, we trust that
|
|
* sentiment.
|
|
*/
|
|
if (dragon[bb].status == ALIVE) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
break;
|
|
}
|
|
|
|
/* It remains the possibility that what we have captured
|
|
* is just a nakade shape. Ask the owl code whether this
|
|
* move saves our attacking dragon.
|
|
*
|
|
* FIXME: Might need to involve semeai code too here.
|
|
*/
|
|
if (owl_does_defend(pos, bb, NULL)) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
}
|
|
break;
|
|
}
|
|
case DEFEND_MOVE:
|
|
case DEFEND_MOVE_GOOD_KO:
|
|
case DEFEND_MOVE_BAD_KO:
|
|
{
|
|
int aa = what;
|
|
|
|
if (dragon[aa].status == ALIVE)
|
|
/* It would be better if this never happened, but it does
|
|
* sometimes. The owl reading can be very slow then.
|
|
*/
|
|
safety = 1;
|
|
|
|
else if (!play_connect_n(color, 0, 1, pos, aa, pos)
|
|
&& owl_does_defend(pos, aa, NULL))
|
|
safety = 1;
|
|
break;
|
|
}
|
|
|
|
case ATTACK_THREAT:
|
|
case DEFEND_THREAT:
|
|
break;
|
|
|
|
case CONNECT_MOVE:
|
|
{
|
|
int worm1 = conn_worm1[move_reasons[r].what];
|
|
int worm2 = conn_worm2[move_reasons[r].what];
|
|
int aa = dragon[worm1].origin;
|
|
int bb = dragon[worm2].origin;
|
|
|
|
if (aa == bb)
|
|
continue;
|
|
|
|
if (DRAGON2(aa).owl_status == ALIVE
|
|
|| DRAGON2(bb).owl_status == ALIVE) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
}
|
|
else if ((DRAGON2(aa).owl_status == UNCHECKED
|
|
&& dragon[aa].crude_status == ALIVE)
|
|
|| (DRAGON2(bb).owl_status == UNCHECKED
|
|
&& dragon[bb].crude_status == ALIVE)) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
}
|
|
else if (owl_connection_defends(pos, aa, bb)) {
|
|
tactical_safety = 1;
|
|
safety = 1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (safety == 1 && (tactical_safety == 1 || safe_move(pos, color)))
|
|
break;
|
|
}
|
|
|
|
if (safety == 1 && (tactical_safety || safe_move(pos, color)))
|
|
move[pos].move_safety = 1;
|
|
else
|
|
move[pos].move_safety = 0;
|
|
|
|
time_report(3, " examine_move_safety: ", pos, 1.0);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the pre-computed weakness of a dragon, with corrections
|
|
* according to ignore_dead_dragons.
|
|
*
|
|
* FIXME: Important to test more exactly how effective a strategical
|
|
* attack or defense of a weak dragon is. This can be done by
|
|
* measuring escape factor and moyo size after the move and
|
|
* compare with the old values. Also necessary to test whether
|
|
* an attack or defense of a critical dragon is effective.
|
|
* Notice that this wouldn't exactly go into this function but
|
|
* rather where it's called.
|
|
*/
|
|
|
|
float
|
|
dragon_weakness(int dr, int ignore_dead_dragons)
|
|
{
|
|
int dragon_safety = DRAGON2(dr).safety;
|
|
|
|
/* Kludge: If a dragon is dead, we return 1.0 in order not
|
|
* to try to run away.
|
|
*/
|
|
if (ignore_dead_dragons
|
|
&& (dragon_safety == DEAD
|
|
|| dragon_safety == INESSENTIAL
|
|
|| dragon_safety == TACTICALLY_DEAD))
|
|
return 0.0;
|
|
|
|
/* When scoring, we don't want to reinforce ALIVE dragons. */
|
|
if (doing_scoring && dragon_safety == ALIVE)
|
|
return 0.0;
|
|
|
|
return DRAGON2(dr).weakness;
|
|
}
|
|
|
|
/*
|
|
* Strategical value of connecting (or cutting) the dragon at (dragona)
|
|
* to the dragon at (dragonb). Notice that this function is asymmetric.
|
|
* This is because connection_value(a, b) is intended to measure the
|
|
* strategical value on the a dragon from a connection to the b dragon.
|
|
*
|
|
* Consider the following position:
|
|
* +---------+
|
|
* |XXO.O.OXX|
|
|
* |.XOOOOOX.|
|
|
* |XXXX.XXXX|
|
|
* |.XOOXOOX.|
|
|
* |XXO.X.O.X|
|
|
* |OOOXXXOOO|
|
|
* |..OOOOO..|
|
|
* |.........|
|
|
* +---------+
|
|
*
|
|
* X has three dragons, one invincible to the left (A), one critical to
|
|
* the right (B), and one dead in the center (C). The move at the cutting
|
|
* point has three move reasons:
|
|
* connect A and B
|
|
* connect A and C
|
|
* connect B and C
|
|
*
|
|
* The strategical value on A of either connection is of course zero,
|
|
* since it's very unconditionally alive. The strategical value on B is
|
|
* high when it's connected to A but small (at least should be) from the
|
|
* connection to C. Similarly for dragon C. In effect the total
|
|
* strategical value of this move is computed as:
|
|
*
|
|
* max(connection_value(A, B), connection_value(A, C))
|
|
* + max(connection_value(B, A), connection_value(B, C))
|
|
* + max(connection_value(C, A), connection_value(C, B))
|
|
*
|
|
* The parameter 'margin' is the margin by which we are ahead.
|
|
* If this exceeds 20 points we value connections more. This is because
|
|
* we can afford to waste a move making sure of safety.
|
|
*/
|
|
|
|
static float
|
|
connection_value(int dragona, int dragonb, int tt, float margin)
|
|
{
|
|
struct dragon_data2 *da = &DRAGON2(dragona);
|
|
struct dragon_data2 *db = &DRAGON2(dragonb);
|
|
float sizea = da->strategic_size;
|
|
float sizeb = db->strategic_size;
|
|
int safetya = da->safety;
|
|
int safetyb = db->safety;
|
|
float crude_weakness_a
|
|
= crude_dragon_weakness(da->safety, &da->genus, da->lunch != NO_MOVE,
|
|
da->moyo_territorial_value,
|
|
(float) da->escape_route);
|
|
float crude_weakness_sum;
|
|
struct eyevalue genus_sum;
|
|
float terr_val = move[tt].territorial_value;
|
|
float return_value;
|
|
|
|
if (margin > 20.0)
|
|
margin = 20.0;
|
|
|
|
/* When scoring, we want to be restrictive with reinforcement moves.
|
|
* Thus if both dragons are alive, strongly alive, or invincible, no
|
|
* bonus is awarded.
|
|
*
|
|
* FIXME: Shouldn't it be sufficient to check this for dragon a?
|
|
*/
|
|
if (doing_scoring) {
|
|
if ((safetya == ALIVE
|
|
|| safetya == STRONGLY_ALIVE
|
|
|| safetya == INVINCIBLE)
|
|
&& (safetyb == ALIVE
|
|
|| safetyb == STRONGLY_ALIVE
|
|
|| safetyb == INVINCIBLE))
|
|
return 0.0;
|
|
}
|
|
|
|
if (safetyb == INESSENTIAL)
|
|
return 0.0;
|
|
|
|
if (crude_weakness_a == 0.0
|
|
|| dragon[dragona].status == DEAD)
|
|
return 0.0;
|
|
if (terr_val < 0.0)
|
|
terr_val = 0.0;
|
|
|
|
add_eyevalues(&da->genus, &db->genus, &genus_sum);
|
|
/* FIXME: There is currently no sane way to take the escape values
|
|
* into account. Hence we simply pretend they do not change.
|
|
*
|
|
* FIXME: terr_val is a very crude approximation to the expected
|
|
* increase in moyo size. It's especially way off if the move at (tt)
|
|
* (owl) defends some stones.
|
|
*/
|
|
crude_weakness_sum
|
|
= crude_dragon_weakness(safetyb, &genus_sum,
|
|
(da->lunch != NO_MOVE || db->lunch != NO_MOVE),
|
|
da->moyo_territorial_value
|
|
+ db->moyo_territorial_value
|
|
+ terr_val,
|
|
(float) da->escape_route);
|
|
|
|
/* Kludge: For a CRITICAL dragon, we use the usual effective
|
|
* size and give a strategic effect bigger than 2.0 * effective size.
|
|
* This is to match the "strategic bonus computation" in
|
|
* estimate_strategical_value(). This prefers connection moves that
|
|
* owl defend a dragon to other owl defense move.
|
|
*/
|
|
if (dragon[dragona].status == CRITICAL) {
|
|
float bonus = (0.4 - 0.3 * crude_weakness_sum) * sizea;
|
|
|
|
if (bonus < 0.0)
|
|
bonus = 0.0;
|
|
|
|
/* If ahead, give extra bonus to connections. */
|
|
if (margin > 0.0 && bonus > 0.0)
|
|
bonus *= 1.0 + 0.05 * margin;
|
|
return_value = 2.0 * sizea + bonus;
|
|
}
|
|
else {
|
|
float old_burden = 2.0 * crude_weakness_a * soft_cap(sizea, 15.0);
|
|
|
|
/* The new burden is the burden of defending new joint dragon; but
|
|
* we share this burden proportionally with the other dragon.
|
|
*/
|
|
float new_burden = 2.0 * crude_weakness_sum * soft_cap(sizea + sizeb, 15.0)
|
|
* sizea / (sizea + sizeb);
|
|
|
|
return_value = 1.05 * (old_burden - new_burden);
|
|
/* If ahead, give extra bonus to connections. */
|
|
if (margin > 0.0)
|
|
return_value *= 1.0 + 0.02 * margin;
|
|
}
|
|
|
|
if (return_value < 0.0)
|
|
return_value = 0.0;
|
|
|
|
return return_value;
|
|
}
|
|
|
|
|
|
/*
|
|
* This function computes the shape factor, which multiplies
|
|
* the score of a move. We take the largest positive contribution
|
|
* to shape and add 1 for each additional positive contribution found.
|
|
* Then we take the largest negative contribution to shape, and
|
|
* add 1 for each additional negative contribution. The resulting
|
|
* number is raised to the power 1.05.
|
|
*
|
|
* The rationale behind this complicated scheme is that every
|
|
* shape point is very significant. If two shape contributions
|
|
* with values (say) 5 and 3 are found, the second contribution
|
|
* should be devalued to 1. Otherwise the engine is too difficult to
|
|
* tune since finding multiple contributions to shape can cause
|
|
* significant overvaluing of a move.
|
|
*/
|
|
|
|
static float
|
|
compute_shape_factor(int pos)
|
|
{
|
|
float exponent = move[pos].maxpos_shape - move[pos].maxneg_shape;
|
|
|
|
ASSERT_ON_BOARD1(pos);
|
|
if (move[pos].numpos_shape > 1)
|
|
exponent += move[pos].numpos_shape - 1;
|
|
if (move[pos].numneg_shape > 1)
|
|
exponent -= move[pos].numneg_shape - 1;
|
|
return pow(1.05, exponent);
|
|
}
|
|
|
|
|
|
/*
|
|
* Usually the value of attacking a worm is twice its effective size,
|
|
* but when evaluating certain move reasons we need to adjust this to
|
|
* take effects on neighbors into account, e.g. for an attack_either
|
|
* move reason. This does not apply to the attack and defense move
|
|
* reasons, however, because then the neighbors already have separate
|
|
* attack or defense move reasons (if such apply).
|
|
*
|
|
* If the worm has an adjacent (friendly) dead dragon we add its
|
|
* value. At least one of the surrounding dragons must be alive.
|
|
* If not, the worm must produce an eye of sufficient size, and that
|
|
* should't be accounted for here. As a guess, we suppose that
|
|
* a critical dragon is alive for our purpose here.
|
|
*
|
|
* On the other hand if it has an adjacent critical worm, and
|
|
* if (pos) does not defend that worm, we subtract the value of the
|
|
* worm, since (pos) may be defended by attacking that worm. We make at
|
|
* most one adjustment of each type.
|
|
*/
|
|
|
|
static float
|
|
adjusted_worm_attack_value(int pos, int ww)
|
|
{
|
|
int num_adj;
|
|
int adjs[MAXCHAIN];
|
|
int has_live_neighbor = 0;
|
|
float adjusted_value = 2 * worm[ww].effective_size;
|
|
float adjustment_up = 0.0;
|
|
float adjustment_down = 0.0;
|
|
int s;
|
|
|
|
num_adj = chainlinks(ww, adjs);
|
|
for (s = 0; s < num_adj; s++) {
|
|
int adj = adjs[s];
|
|
|
|
if (dragon[adj].status != DEAD)
|
|
has_live_neighbor = 1;
|
|
|
|
if (dragon[adj].status == DEAD
|
|
&& 2*dragon[adj].effective_size > adjustment_up)
|
|
adjustment_up = 2*dragon[adj].effective_size;
|
|
|
|
if (worm[adj].attack_codes[0] != 0
|
|
&& !does_defend(pos, adj)
|
|
&& 2*worm[adj].effective_size > adjustment_down)
|
|
adjustment_down = 2*worm[adj].effective_size;
|
|
}
|
|
|
|
if (has_live_neighbor)
|
|
adjusted_value += adjustment_up;
|
|
adjusted_value -= adjustment_down;
|
|
|
|
/* It can happen that the adjustment down was larger than the effective
|
|
* size we started with. In this case we simply return 0.0. (This means
|
|
* we ignore the respective EITHER_MOVE reason.)
|
|
*/
|
|
if (adjusted_value > 0.0)
|
|
return adjusted_value;
|
|
else
|
|
return 0.0;
|
|
}
|
|
|
|
|
|
/* The new (3.2) territorial evaluation overvalues moves creating a new
|
|
* group in the opponent's sphere of influence. The influence module cannot
|
|
* see that the opponent will gain by attacking the new (probably weak)
|
|
* group.
|
|
* This function uses some heuristics to estimate the strategic penalty
|
|
* of invasion moves, and moves that try to run away with a group of size
|
|
* 1 in front of opponent's strength.
|
|
*/
|
|
static float
|
|
strategic_penalty(int pos, int color)
|
|
{
|
|
int k;
|
|
float ret_val;
|
|
|
|
/* We try to detect support from an alive friendly stone by checking
|
|
* whether all neighboring intersections belong to the opponent's moyo.
|
|
*/
|
|
for (k = 0; k < 4; k++)
|
|
if (board[pos + delta[k]] == EMPTY
|
|
&& whose_area(OPPOSITE_INFLUENCE(color), pos + delta[k])
|
|
!= OTHER_COLOR(color))
|
|
return 0.0;
|
|
if (whose_area(OPPOSITE_INFLUENCE(color), pos) != OTHER_COLOR(color))
|
|
return 0.0;
|
|
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos].reason[k];
|
|
if (r < 0)
|
|
break;
|
|
/* We assume that invasion moves can only have the move reasons listed
|
|
* below.
|
|
*
|
|
* FIXME: EXPAND_TERRITORY should always be connected to our own
|
|
* stones. Remove later when that change is done.
|
|
*/
|
|
switch (move_reasons[r].type) {
|
|
#if 0
|
|
case EXPAND_TERRITORY_MOVE:
|
|
#endif
|
|
case EXPAND_MOYO_MOVE:
|
|
case STRATEGIC_ATTACK_MOVE:
|
|
case INVASION_MOVE:
|
|
continue;
|
|
/* If we find a tactical defense move, we just test whether it concerns
|
|
* a single-stone-dragon; if not, we stop, if yes, we let the necessary
|
|
* tests be made in the OWL_DEFEND_MOVE case.
|
|
*/
|
|
case DEFEND_MOVE:
|
|
{
|
|
int target = move_reasons[r].what;
|
|
if (dragon[target].size > 1)
|
|
return 0.0;
|
|
continue;
|
|
}
|
|
/* An owl defense of a single stone might be a stupid attempt to run
|
|
* away with an unimportant (kikashi like) stone. We assume this is the
|
|
* case if this single stone has a strong hostile direct neighbor.
|
|
*/
|
|
case OWL_DEFEND_MOVE:
|
|
{
|
|
int target = move_reasons[r].what;
|
|
int has_strong_neighbor = 0;
|
|
int has_weak_neighbor = 0;
|
|
int i;
|
|
/* We award no penalty for running away with a cutting stone. */
|
|
if (dragon[target].size > 1
|
|
|| worm[target].cutstone > 0
|
|
|| worm[target].cutstone2 > 0)
|
|
return 0.0;
|
|
/* Third line moves (or lower) are ok -- they try to live, not run
|
|
* away.
|
|
*/
|
|
if (edge_distance(pos) < 3)
|
|
return 0.0;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
if (board[target + delta[i]] == OTHER_COLOR(color)) {
|
|
if (dragon[target + delta[i]].size == 1) {
|
|
has_weak_neighbor = 1;
|
|
break;
|
|
}
|
|
switch (DRAGON2(target + delta[i]).safety) {
|
|
case INVINCIBLE:
|
|
case STRONGLY_ALIVE:
|
|
has_strong_neighbor = 1;
|
|
break;
|
|
case ALIVE:
|
|
if (DRAGON2(target + delta[i]).weakness > 0.4)
|
|
has_weak_neighbor = 1;
|
|
break;
|
|
default:
|
|
has_weak_neighbor = 1;
|
|
}
|
|
}
|
|
if (has_weak_neighbor || (!has_strong_neighbor))
|
|
return 0.0;
|
|
else
|
|
continue;
|
|
}
|
|
default:
|
|
return 0.0;
|
|
}
|
|
}
|
|
|
|
/* We have to make a guess how much the point where we want to play
|
|
* is dominated by the opponent. The territorial valuation is a
|
|
* good try here.
|
|
*/
|
|
ret_val = influence_territory(INITIAL_INFLUENCE(OTHER_COLOR(color)),
|
|
pos, OTHER_COLOR(color));
|
|
ret_val *= 12.0;
|
|
ret_val = gg_max(0.0, ret_val);
|
|
return ret_val;
|
|
}
|
|
|
|
|
|
/* True if pos is adjacent to a nondead stone of the given color. This
|
|
* function can be called when stackp>0 but the result is given for
|
|
* the position when stackp==0. It also checks for nondead stones two
|
|
* steps away from pos if a move by color at pos cannot be cut off
|
|
* from that stone.
|
|
*
|
|
* FIXME: Move this somewhere more generally accessible, probably
|
|
* utils.c
|
|
*/
|
|
int
|
|
adjacent_to_nondead_stone(int pos, int color)
|
|
{
|
|
int k;
|
|
|
|
int stack[MAXSTACK];
|
|
int move_color[MAXSTACK];
|
|
int saved_stackp = stackp;
|
|
int result = 0;
|
|
|
|
while (stackp > 0) {
|
|
get_move_from_stack(stackp - 1, &stack[stackp - 1],
|
|
&move_color[stackp - 1]);
|
|
popgo();
|
|
}
|
|
|
|
if (trymove(pos, color, NULL, EMPTY)) {
|
|
for (k = 0; k < 12; k++) {
|
|
int pos2;
|
|
if (k < 8)
|
|
pos2 = pos + delta[k];
|
|
else if (ON_BOARD(pos + delta[k - 8]))
|
|
pos2 = pos + 2 * delta[k - 8];
|
|
else
|
|
continue;
|
|
|
|
if (ON_BOARD(pos2)
|
|
&& worm[pos2].color == color
|
|
&& dragon[pos2].status != DEAD
|
|
&& !disconnect(pos, pos2, NULL)) {
|
|
result = 1;
|
|
break;
|
|
}
|
|
}
|
|
popgo();
|
|
}
|
|
|
|
while (stackp < saved_stackp)
|
|
tryko(stack[stackp], move_color[stackp], NULL);
|
|
|
|
return result;
|
|
}
|
|
|
|
static int
|
|
max_lunch_eye_value(int pos)
|
|
{
|
|
int min;
|
|
int probable;
|
|
int max;
|
|
|
|
estimate_lunch_eye_value(pos, &min, &probable, &max, 0);
|
|
return max;
|
|
}
|
|
|
|
/*
|
|
* Estimate the direct territorial value of a move at (pos) by (color).
|
|
*/
|
|
static void
|
|
estimate_territorial_value(int pos, int color, float our_score,
|
|
int disable_delta_territory_cache)
|
|
{
|
|
int other = OTHER_COLOR(color);
|
|
int k;
|
|
int aa = NO_MOVE;
|
|
int bb = NO_MOVE;
|
|
|
|
float this_value = 0.0;
|
|
float tot_value = 0.0;
|
|
float secondary_value = 0.0;
|
|
|
|
int does_block = 0;
|
|
signed char safe_stones[BOARDMAX];
|
|
float strength[BOARDMAX];
|
|
|
|
set_strength_data(OTHER_COLOR(color), safe_stones, strength);
|
|
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos].reason[k];
|
|
if (r < 0)
|
|
break;
|
|
if (move_reasons[r].status & TERRITORY_REDUNDANT)
|
|
continue;
|
|
|
|
this_value = 0.0;
|
|
switch (move_reasons[r].type) {
|
|
case ATTACK_MOVE:
|
|
case ATTACK_MOVE_GOOD_KO:
|
|
case ATTACK_MOVE_BAD_KO:
|
|
aa = move_reasons[r].what;
|
|
|
|
ASSERT1(board[aa] != color, aa);
|
|
|
|
/* Defenseless stone. */
|
|
if (worm[aa].defense_codes[0] == 0) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f (secondary) - attack on %1m (defenseless)\n",
|
|
pos, worm[aa].effective_size, aa);
|
|
secondary_value += worm[aa].effective_size;
|
|
does_block = 1;
|
|
break;
|
|
}
|
|
|
|
this_value = 2 * worm[aa].effective_size;
|
|
|
|
/* If the stones are dead, there is only a secondary value in
|
|
* capturing them tactically as well.
|
|
*/
|
|
if (dragon[aa].status == DEAD) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f (secondary) - attack on %1m (dead)\n",
|
|
pos, 0.2 * this_value, aa);
|
|
secondary_value += 0.2 * this_value;
|
|
does_block = 1;
|
|
break;
|
|
}
|
|
|
|
/* Mark the string as captured, for evaluation in the influence code. */
|
|
mark_changed_string(aa, safe_stones, strength, 0);
|
|
TRACE(" %1m: attack on worm %1m\n", pos, aa);
|
|
|
|
/* FIXME: How much should we reduce the value for ko attacks? */
|
|
if (move_reasons[r].type == ATTACK_MOVE)
|
|
this_value = 0.0;
|
|
else if (move_reasons[r].type == ATTACK_MOVE_GOOD_KO) {
|
|
this_value *= 0.3;
|
|
TRACE(" %1m: -%f - attack on worm %1m only with good ko\n",
|
|
pos, this_value, aa);
|
|
}
|
|
else if (move_reasons[r].type == ATTACK_MOVE_BAD_KO) {
|
|
this_value *= 0.5;
|
|
TRACE(" %1m: -%f - attack on worm %1m only with bad ko\n",
|
|
pos, this_value, aa);
|
|
}
|
|
|
|
tot_value -= this_value;
|
|
does_block = 1;
|
|
break;
|
|
|
|
case DEFEND_MOVE:
|
|
case DEFEND_MOVE_GOOD_KO:
|
|
case DEFEND_MOVE_BAD_KO:
|
|
aa = move_reasons[r].what;
|
|
|
|
ASSERT1(board[aa] == color, aa);
|
|
|
|
/*
|
|
* Estimate value
|
|
*/
|
|
this_value = 2 * worm[aa].effective_size;
|
|
|
|
/* If the stones are dead, we use the convention that
|
|
* defending them has a strategical value rather than
|
|
* territorial. Admittedly this make more sense for attacks on
|
|
* dead stones.
|
|
*/
|
|
if (dragon[aa].status == DEAD) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f (secondary) - defense of %1m (dead)\n",
|
|
pos, 0.2 * this_value, aa);
|
|
secondary_value += 0.2 * this_value;
|
|
break;
|
|
}
|
|
|
|
if (DRAGON2(aa).owl_status == CRITICAL
|
|
&& (owl_defense_move_reason_known(pos, aa)
|
|
< defense_move_reason_known(pos, aa))
|
|
&& !semeai_move_reason_known(pos, aa)) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f (secondary) - ineffective defense of %1m (critical)\n",
|
|
pos, 0.2 * this_value, aa);
|
|
secondary_value += 0.2 * this_value;
|
|
break;
|
|
}
|
|
|
|
/* Mark the string as saved, for evaluation in the influence code. */
|
|
mark_changed_string(aa, safe_stones, strength, INFLUENCE_SAVED_STONE);
|
|
TRACE(" %1m: defense of worm %1m\n", pos, aa);
|
|
|
|
/* FIXME: How much should we reduce the value for ko defenses? */
|
|
if (move_reasons[r].type == DEFEND_MOVE)
|
|
this_value = 0.0;
|
|
else if (move_reasons[r].type == DEFEND_MOVE_GOOD_KO) {
|
|
this_value *= 0.3;
|
|
TRACE(" %1m: -%f - defense of worm %1m with good ko\n",
|
|
pos, this_value, aa);
|
|
}
|
|
else if (move_reasons[r].type == DEFEND_MOVE_BAD_KO) {
|
|
this_value *= 0.5;
|
|
TRACE(" %1m: -%f - defense of worm %1m with bad ko\n",
|
|
pos, this_value, aa);
|
|
}
|
|
|
|
tot_value -= this_value;
|
|
|
|
/* If a move tactically defends an owl critical string, but
|
|
* this move is not listed as an owl defense, it probably is
|
|
* ineffective. The 0.45 factor is chosen so that even in
|
|
* combination with bad ko it still has a positive net impact.
|
|
*/
|
|
if (DRAGON2(aa).owl_status == CRITICAL
|
|
&& (owl_defense_move_reason_known(pos, aa)
|
|
< defense_move_reason_known(pos, aa))) {
|
|
this_value = 0.45 * (2 * worm[aa].effective_size);
|
|
TRACE(" %1m: -%f - suspected ineffective defense of worm %1m\n",
|
|
pos, this_value, aa);
|
|
tot_value -= this_value;
|
|
}
|
|
|
|
does_block = 1;
|
|
break;
|
|
|
|
case ATTACK_THREAT:
|
|
aa = move_reasons[r].what;
|
|
|
|
/* Make sure this is a threat to attack opponent stones. */
|
|
ASSERT1(board[aa] == other, aa);
|
|
|
|
if (dragon[aa].status == DEAD) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: 0.0 - threatens to capture %1m (dead)\n", pos, aa);
|
|
break;
|
|
}
|
|
|
|
/* The followup value of a move threatening to attack (aa)
|
|
* is twice its effective size, with adjustments. If the
|
|
* worm has an adjacent (friendly) dead dragon we add its
|
|
* value. On the other hand if it has an adjacent critical
|
|
* worm, and if (pos) does not defend that worm, we subtract
|
|
* the value of the worm, since (aa) may be defended by
|
|
* attacking that worm. We make at most one adjustment
|
|
* of each type.
|
|
*
|
|
* No followup value is awarded if the defense move is a threat
|
|
* back on our move because we're likely to end in gote then,
|
|
* unless the move is unsafe anyway and played as a ko threat.
|
|
*
|
|
* FIXME: It might be possible that parts of the dragon
|
|
* can be cut in the process of capturing the (aa)
|
|
* worm. In that case, not the entire size of the
|
|
* adjacent dead dragon should be counted as a positive
|
|
* adjustment. However, it seems difficult to do this
|
|
* analysis, and in most cases it won't apply, so we
|
|
* leave it as it is for now.
|
|
*
|
|
* FIXME: The same analysis should be applied to
|
|
* DEFEND_THREAT,
|
|
* ATTACK_EITHER_MOVE, DEFEND_BOTH_MOVE. It should be
|
|
* broken out as separate functions and dealt with in
|
|
* a structured manner.
|
|
*/
|
|
|
|
if (trymove(pos, color, "estimate_territorial_value-A", NO_MOVE)) {
|
|
int adjs[MAXCHAIN];
|
|
float adjusted_value = 2 * worm[aa].effective_size;
|
|
float adjustment_up = 0.0;
|
|
float adjustment_down = 0.0;
|
|
int s;
|
|
int num_adj;
|
|
int defense_move;
|
|
|
|
/* In rare cases it may happen that the trymove() above
|
|
* actually removed the string at aa.
|
|
*/
|
|
if (board[aa] == EMPTY)
|
|
num_adj = 0;
|
|
else
|
|
num_adj = chainlinks(aa, adjs);
|
|
|
|
/* No followup value if string can be defended with threat
|
|
* against our move. An exception to this is when our move
|
|
* isn't safe anyway and we play this only for the followup
|
|
* value, typically as a ko threat. Though, "suspicious" owl
|
|
* defenses (move_safety != 1) are still tested for possible
|
|
* backfires.
|
|
*
|
|
* This rule may be overwritten with patterns. See pattern
|
|
* Sente22 and related test trevord:950 for an example.
|
|
*
|
|
* FIXME: This is somewhat halfhearted since only one defense
|
|
* move is tested.
|
|
*/
|
|
if (!is_known_good_attack_threat(pos, aa)
|
|
&& board[aa] != EMPTY
|
|
&& (move[pos].move_safety == 1
|
|
|| adjacent_to_nondead_stone(pos, color)
|
|
|| owl_defense_move_reason_known(pos, -1))
|
|
&& find_defense(aa, &defense_move) == WIN
|
|
&& defense_move != NO_MOVE) {
|
|
int bad_followup;
|
|
int attack_move;
|
|
|
|
if (attack(pos, &attack_move) != WIN) {
|
|
int i;
|
|
|
|
if (trymove(defense_move, other,
|
|
"estimate_territorial_value-b", NO_MOVE)) {
|
|
if (board[pos] == EMPTY || attack(pos, NULL) != 0) {
|
|
popgo();
|
|
popgo();
|
|
break;
|
|
}
|
|
|
|
/* Now check all `ATTACK_MOVE' reasons for this same
|
|
* move. If the defense against current threat makes a
|
|
* string attacked by this move defendable, we reduce
|
|
* the followup.
|
|
*
|
|
* Adjustments done later are concerned with current
|
|
* dragon states. Here we actually try to check if
|
|
* opponent's reply to our move will have a followup in
|
|
* turn.
|
|
*/
|
|
for (i = 0; i < MAX_REASONS; i++) {
|
|
int reason = move[pos].reason[i];
|
|
int attacked_string;
|
|
if (reason < 0)
|
|
break;
|
|
|
|
attacked_string = move_reasons[reason].what;
|
|
if (move_reasons[reason].type == ATTACK_MOVE
|
|
&& board[attacked_string] == other) {
|
|
int defense_code = find_defense(attacked_string, NULL);
|
|
double down_coefficient = 0.0;
|
|
|
|
switch (defense_code) {
|
|
case WIN:
|
|
down_coefficient = 2.0;
|
|
break;
|
|
|
|
case KO_A:
|
|
down_coefficient = 2.0 * 0.5;
|
|
break;
|
|
|
|
case KO_B:
|
|
down_coefficient = 2.0 * 0.7;
|
|
break;
|
|
}
|
|
|
|
if (adjustment_down
|
|
< (worm[attacked_string].effective_size
|
|
* down_coefficient)) {
|
|
adjustment_down = (worm[attacked_string].effective_size
|
|
* down_coefficient);
|
|
}
|
|
}
|
|
}
|
|
|
|
popgo();
|
|
}
|
|
}
|
|
else {
|
|
/* Our move is attackable to begin with. However, maybe
|
|
* the attack is not sufficient to defend opponent's
|
|
* string?
|
|
*/
|
|
if (trymove(attack_move, other,
|
|
"estimate_territorial_value-c", NO_MOVE)) {
|
|
if (attack(aa, NULL) == 0) {
|
|
/* It is sufficient, no followup. */
|
|
popgo();
|
|
popgo();
|
|
break;
|
|
}
|
|
|
|
popgo();
|
|
}
|
|
|
|
/* Heuristically reduce the followup, since our string
|
|
* will be still attackable if opponent defends his
|
|
* string.
|
|
*/
|
|
adjustment_down = 2 * countstones(pos);
|
|
}
|
|
|
|
bad_followup = 0;
|
|
for (s = 0; s < num_adj; s++) {
|
|
int lib;
|
|
if (countlib(adjs[s]) == 1) {
|
|
findlib(adjs[s], 1, &lib);
|
|
if (trymove(lib, other,
|
|
"estimate_territorial_value-d", NO_MOVE)) {
|
|
if (!attack(aa, NULL)
|
|
&& (board[pos] == EMPTY || attack(pos, NULL) != 0)) {
|
|
popgo();
|
|
bad_followup = 1;
|
|
break;
|
|
}
|
|
popgo();
|
|
}
|
|
}
|
|
}
|
|
if (bad_followup) {
|
|
popgo();
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (s = 0; s < num_adj; s++) {
|
|
int adj = adjs[s];
|
|
|
|
if (same_string(pos, adj))
|
|
continue;
|
|
if (dragon[adj].color == color
|
|
&& dragon[adj].status == DEAD
|
|
&& 2*dragon[adj].effective_size > adjustment_up)
|
|
adjustment_up = 2*dragon[adj].effective_size;
|
|
if (dragon[adj].color == color
|
|
&& attack(adj, NULL)
|
|
&& 2*worm[adj].effective_size > adjustment_down)
|
|
adjustment_down = 2*worm[adj].effective_size;
|
|
}
|
|
|
|
popgo();
|
|
|
|
/* No followup if the string is not substantial. */
|
|
{
|
|
int save_verbose = verbose;
|
|
if (verbose > 0)
|
|
verbose --;
|
|
if (move[pos].move_safety == 0
|
|
&& !owl_substantial(aa)) {
|
|
verbose = save_verbose;
|
|
break;
|
|
}
|
|
verbose = save_verbose;
|
|
}
|
|
|
|
adjusted_value += adjustment_up;
|
|
adjusted_value -= adjustment_down;
|
|
if (adjusted_value > 0.0) {
|
|
add_followup_value(pos, adjusted_value);
|
|
TRACE(" %1m: %f (followup) - threatens to capture %1m\n",
|
|
pos, adjusted_value, aa);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DEFEND_THREAT:
|
|
aa = move_reasons[r].what;
|
|
|
|
/* Make sure this is a threat to defend our stones. */
|
|
ASSERT1(board[aa] == color, aa);
|
|
|
|
/* We don't trust tactical defense threats as ko threats, unless
|
|
* the move is safe.
|
|
*/
|
|
if (move[pos].move_safety == 0)
|
|
break;
|
|
|
|
/* No followup value if string can be attacked with threat
|
|
* against our move. An exception to this is when our move
|
|
* isn't safe anyway and we play this only for the followup
|
|
* value, typically as a ko threat.
|
|
*
|
|
* FIXME: This is somewhat halfhearted since only one attack
|
|
* move is tested.
|
|
*/
|
|
if (trymove(pos, color, "estimate_territorial_value-A", NO_MOVE)) {
|
|
int attack_move;
|
|
if (move[pos].move_safety == 1
|
|
&& attack(aa, &attack_move) == WIN
|
|
&& attack_move != NO_MOVE) {
|
|
if (trymove(attack_move, other,
|
|
"estimate_territorial_value-b", NO_MOVE)) {
|
|
if (board[pos] == EMPTY || attack(pos, NULL) != 0) {
|
|
popgo();
|
|
popgo();
|
|
break;
|
|
}
|
|
popgo();
|
|
}
|
|
}
|
|
popgo();
|
|
}
|
|
|
|
add_followup_value(pos, 2 * worm[aa].effective_size);
|
|
|
|
TRACE(" %1m: %f (followup) - threatens to defend %1m\n",
|
|
pos, 2 * worm[aa].effective_size, aa);
|
|
|
|
break;
|
|
|
|
case UNCERTAIN_OWL_DEFENSE:
|
|
/* This move reason is valued as a strategical value. */
|
|
break;
|
|
|
|
case CUT_MOVE:
|
|
case EXPAND_MOYO_MOVE:
|
|
case EXPAND_TERRITORY_MOVE:
|
|
case INVASION_MOVE:
|
|
does_block = 1;
|
|
break;
|
|
|
|
case CONNECT_MOVE:
|
|
/* This used to always set does_block=1, but there is no
|
|
* guarantee that a connection move is strategically safe. See
|
|
* for example gunnar:72.
|
|
*/
|
|
if (move[pos].move_safety)
|
|
does_block = 1;
|
|
break;
|
|
|
|
case STRATEGIC_ATTACK_MOVE:
|
|
case STRATEGIC_DEFEND_MOVE:
|
|
/* Do not trust these when we are scoring. Maybe we shouldn't
|
|
* trust them otherwise either but require them to be
|
|
* accompanied by e.g. an EXPAND move reason.
|
|
*/
|
|
if (!doing_scoring)
|
|
does_block = 1;
|
|
break;
|
|
|
|
case SEMEAI_THREAT:
|
|
aa = move_reasons[r].what;
|
|
|
|
/* threaten to win the semeai as a ko threat */
|
|
add_followup_value(pos, 2 * dragon[aa].effective_size);
|
|
TRACE(" %1m: %f (followup) - threatens to win semeai for %1m\n",
|
|
pos, 2 * dragon[aa].effective_size, aa);
|
|
break;
|
|
|
|
case SEMEAI_MOVE:
|
|
case OWL_ATTACK_MOVE:
|
|
case OWL_ATTACK_MOVE_GOOD_KO:
|
|
case OWL_ATTACK_MOVE_BAD_KO:
|
|
case OWL_ATTACK_MOVE_GAIN:
|
|
case OWL_DEFEND_MOVE:
|
|
case OWL_DEFEND_MOVE_GOOD_KO:
|
|
case OWL_DEFEND_MOVE_BAD_KO:
|
|
case OWL_DEFEND_MOVE_LOSS:
|
|
|
|
if (move_reasons[r].type == OWL_ATTACK_MOVE_GAIN
|
|
|| move_reasons[r].type == OWL_DEFEND_MOVE_LOSS) {
|
|
aa = either_data[move_reasons[r].what].what1;
|
|
bb = either_data[move_reasons[r].what].what2;
|
|
}
|
|
else {
|
|
aa = move_reasons[r].what;
|
|
bb = NO_MOVE;
|
|
}
|
|
|
|
/* If the dragon is a single ko stone, the owl code currently
|
|
* won't detect that the owl attack is conditional. As a
|
|
* workaround we deduct 0.5 points for the move here, but only
|
|
* if the move is a liberty of the string.
|
|
*/
|
|
if (dragon[aa].size == 1
|
|
&& is_ko_point(aa)
|
|
&& liberty_of_string(pos, aa)) {
|
|
TRACE(" %1m: -0.5 - penalty for ko stone %1m (workaround)\n",
|
|
pos, aa);
|
|
tot_value -= 0.5;
|
|
}
|
|
|
|
/* Mark the affected dragon for use in the territory analysis. */
|
|
mark_changed_dragon(pos, color, aa, bb, move_reasons[r].type,
|
|
safe_stones, strength, &this_value);
|
|
this_value *= 2.0;
|
|
|
|
TRACE(" %1m: owl attack/defend for %1m\n", pos, aa);
|
|
|
|
/* FIXME: How much should we reduce the value for ko attacks? */
|
|
if (move_reasons[r].type == OWL_ATTACK_MOVE
|
|
|| move_reasons[r].type == OWL_DEFEND_MOVE
|
|
|| move_reasons[r].type == SEMEAI_MOVE)
|
|
this_value = 0.0;
|
|
else if (move_reasons[r].type == OWL_ATTACK_MOVE_GOOD_KO
|
|
|| move_reasons[r].type == OWL_DEFEND_MOVE_GOOD_KO) {
|
|
this_value *= 0.3;
|
|
TRACE(" %1m: -%f - owl attack/defense of %1m only with good ko\n",
|
|
pos, this_value, aa);
|
|
}
|
|
else if (move_reasons[r].type == OWL_ATTACK_MOVE_BAD_KO
|
|
|| move_reasons[r].type == OWL_DEFEND_MOVE_BAD_KO) {
|
|
this_value *= 0.5;
|
|
TRACE(" %1m: -%f - owl attack/defense of %1m only with bad ko\n",
|
|
pos, this_value, aa);
|
|
}
|
|
else if (move_reasons[r].type == OWL_ATTACK_MOVE_GAIN
|
|
|| move_reasons[r].type == OWL_DEFEND_MOVE_LOSS) {
|
|
this_value = 0.0;
|
|
}
|
|
|
|
tot_value -= this_value;
|
|
|
|
/* If the dragon is a single string which can be tactically
|
|
* attacked, but this owl attack does not attack tactically, it
|
|
* can be suspected to leave some unnecessary aji or even be an
|
|
* owl misread. Therefore we give it a small penalty to favor
|
|
* the moves which do attack tactically as well.
|
|
*
|
|
* One example is manyfaces:2 where the single stone S15 can be
|
|
* tactically attacked at S16 but where 3.3.2 finds additional
|
|
* owl attacks at R14 (clearly ineffective) and T15 (might work,
|
|
* but leaves huge amounts of aji).
|
|
*/
|
|
if ((move_reasons[r].type == OWL_ATTACK_MOVE
|
|
|| move_reasons[r].type == OWL_ATTACK_MOVE_GOOD_KO
|
|
|| move_reasons[r].type == OWL_ATTACK_MOVE_BAD_KO)
|
|
&& dragon[aa].size == worm[aa].size
|
|
&& worm[aa].attack_codes[0] == WIN
|
|
&& worm[aa].defense_codes[0] != 0
|
|
&& attack_move_reason_known(pos, aa) != WIN) {
|
|
if (large_scale)
|
|
this_value = (2.0 + 0.05 * (2 * worm[aa].effective_size));
|
|
else
|
|
this_value = 0.05 * (2 * worm[aa].effective_size);
|
|
TRACE(" %1m: -%f - suspected ineffective owl attack of worm %1m\n",
|
|
pos, this_value, aa);
|
|
tot_value -= this_value;
|
|
}
|
|
|
|
does_block = 1;
|
|
break;
|
|
|
|
case OWL_ATTACK_THREAT:
|
|
aa = move_reasons[r].what;
|
|
|
|
if (dragon[aa].status == DEAD) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: 0.0 - threatens to owl attack %1m (dead)\n", pos, aa);
|
|
break;
|
|
}
|
|
|
|
/* The followup value of a move threatening to attack (aa) is
|
|
* twice its effective size, unless it has an adjacent
|
|
* (friendly) critical dragon. In that case it's probably a
|
|
* mistake to make the threat since it can defend itself with
|
|
* profit.
|
|
*
|
|
* FIXME: We probably need to verify that the critical dragon is
|
|
* substantial enough that capturing it saves the threatened
|
|
* dragon.
|
|
*/
|
|
{
|
|
float value = 2 * dragon[aa].effective_size;
|
|
int s;
|
|
|
|
for (s = 0; s < DRAGON2(aa).neighbors; s++) {
|
|
int d = DRAGON2(aa).adjacent[s];
|
|
int adj = dragon2[d].origin;
|
|
|
|
if (dragon[adj].color == color
|
|
&& dragon[adj].status == CRITICAL
|
|
&& dragon2[d].safety != INESSENTIAL
|
|
&& !owl_defense_move_reason_known(pos, adj))
|
|
value = 0.0;
|
|
}
|
|
|
|
if (value > 0.0) {
|
|
add_followup_value(pos, value);
|
|
TRACE(" %1m: %f (followup) - threatens to owl attack %1m\n",
|
|
pos, value, aa);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case OWL_DEFEND_THREAT:
|
|
aa = move_reasons[r].what;
|
|
|
|
add_followup_value(pos, 2 * dragon[aa].effective_size);
|
|
TRACE(" %1m: %f (followup) - threatens to owl defend %1m\n",
|
|
pos, 2 * dragon[aa].effective_size, aa);
|
|
break;
|
|
|
|
case OWL_PREVENT_THREAT:
|
|
/* A move attacking a dragon whose defense can be threatened.
|
|
*/
|
|
aa = move_reasons[r].what;
|
|
|
|
if (dragon[aa].status != DEAD) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: 0.0 - prevent defense threat (dragon is not dead)\n",
|
|
pos);
|
|
break;
|
|
}
|
|
|
|
/* If the opponent just added a stone to a dead dragon, then
|
|
* attack it. If we are ahead, add a safety move here, at most
|
|
* half the margin of victory.
|
|
*
|
|
* This does not apply if we are doing scoring.
|
|
*/
|
|
if (!doing_scoring
|
|
&& is_same_dragon(get_last_opponent_move(color), aa)) {
|
|
this_value = 1.5 * dragon[aa].effective_size;
|
|
TRACE(" %1m: %f - attack last move played, although it seems dead\n",
|
|
pos, this_value);
|
|
tot_value += this_value * attack_dragon_weight;
|
|
}
|
|
else if (!doing_scoring && our_score > 0.0) {
|
|
/* tm - devalued this bonus (3.1.17) */
|
|
this_value = gg_min(0.9 * dragon[aa].effective_size,
|
|
our_score/2.0 - board_size/2.0 - 1.0);
|
|
this_value = gg_max(this_value, 0);
|
|
TRACE(" %1m: %f - attack %1m, although it seems dead, as we are ahead\n",
|
|
pos, this_value, aa);
|
|
tot_value += this_value * attack_dragon_weight;
|
|
}
|
|
else {
|
|
|
|
add_reverse_followup_value(pos, 2 * dragon[aa].effective_size);
|
|
if (board[aa] == color)
|
|
TRACE(" %1m: %f (reverse followup) - prevent threat to attack %1m\n",
|
|
pos, 2 * dragon[aa].effective_size, aa);
|
|
else
|
|
TRACE(" %1m: %f (reverse followup) - prevent threat to defend %1m\n",
|
|
pos, 2 * dragon[aa].effective_size, aa);
|
|
}
|
|
break;
|
|
|
|
case MY_ATARI_ATARI_MOVE:
|
|
/* Add 1.0 to compensate for -1.0 penalty because the move is
|
|
* thought to be a sacrifice.
|
|
*/
|
|
this_value = move_reasons[r].what + 1.0;
|
|
tot_value += this_value;
|
|
TRACE(" %1m: %f - combination attack kills one of several worms\n",
|
|
pos, this_value);
|
|
break;
|
|
|
|
case YOUR_ATARI_ATARI_MOVE:
|
|
/* Set does_block to force territorial valuation of the move.
|
|
* That way we can prefer defenses against combination attacks
|
|
* on dame points instead of inside territory.
|
|
*/
|
|
does_block = 1;
|
|
this_value = move_reasons[r].what;
|
|
tot_value += this_value;
|
|
TRACE(" %1m: %f - defends against combination attack on several worms\n",
|
|
pos, this_value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Currently no difference in the valuation between blocking and
|
|
* expanding moves.
|
|
*/
|
|
this_value = 0.0;
|
|
|
|
mark_inessential_stones(OTHER_COLOR(color), safe_stones);
|
|
|
|
if (move[pos].move_safety == 1
|
|
&& (is_known_safe_move(pos) || safe_move(pos, color) != 0)) {
|
|
safe_stones[pos] = INFLUENCE_SAVED_STONE;
|
|
strength[pos] = DEFAULT_STRENGTH;
|
|
if (0)
|
|
TRACE(" %1m: is a safe move\n", pos);
|
|
}
|
|
else {
|
|
TRACE(" %1m: not a safe move\n", pos);
|
|
safe_stones[pos] = 0;
|
|
strength[pos] = 0.0;
|
|
}
|
|
|
|
/* We don't check for move safety here. This enables a territorial
|
|
* evaluation for sacrifice moves that enable a break-through (or
|
|
* an owl defense).
|
|
*/
|
|
if (does_block
|
|
&& tryko(pos, color, "estimate_territorial_value")) {
|
|
Hash_data safety_hash = goal_to_hashvalue(safe_stones);
|
|
if (disable_delta_territory_cache
|
|
|| !retrieve_delta_territory_cache(pos, color, &this_value,
|
|
&move[pos].influence_followup_value,
|
|
OPPOSITE_INFLUENCE(color),
|
|
safety_hash)) {
|
|
|
|
compute_influence(OTHER_COLOR(color), safe_stones, strength,
|
|
&move_influence, pos, "after move");
|
|
increase_depth_values();
|
|
break_territories(OTHER_COLOR(color), &move_influence, 0, pos);
|
|
decrease_depth_values();
|
|
this_value = influence_delta_territory(OPPOSITE_INFLUENCE(color),
|
|
&move_influence, color, pos);
|
|
compute_followup_influence(&move_influence, &followup_influence,
|
|
pos, "followup");
|
|
|
|
if (this_value != 0.0)
|
|
TRACE("%1m: %f - change in territory\n", pos, this_value);
|
|
else
|
|
DEBUG(DEBUG_MOVE_REASONS, "%1m: 0.00 - change in territory\n", pos);
|
|
move[pos].influence_followup_value
|
|
= influence_delta_territory(&move_influence, &followup_influence,
|
|
color, pos);
|
|
store_delta_territory_cache(pos, color, this_value,
|
|
move[pos].influence_followup_value,
|
|
OPPOSITE_INFLUENCE(color), safety_hash);
|
|
}
|
|
else {
|
|
if (this_value != 0.0)
|
|
TRACE("%1m: %f - change in territory (cached)\n", pos, this_value);
|
|
else
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
"%1m: 0.00 - change in territory (cached)\n", pos);
|
|
}
|
|
popgo();
|
|
|
|
}
|
|
|
|
tot_value += this_value;
|
|
|
|
/* Test if min_territory or max_territory values constrain the
|
|
* delta_territory value.
|
|
*/
|
|
if (tot_value < move[pos].min_territory
|
|
&& move[pos].min_territory > 0) {
|
|
tot_value = move[pos].min_territory;
|
|
TRACE(" %1m: %f - revised to meet minimum territory value\n",
|
|
pos, tot_value);
|
|
}
|
|
if (tot_value > move[pos].max_territory) {
|
|
tot_value = move[pos].max_territory;
|
|
TRACE(" %1m: %f - revised to meet maximum territory value\n",
|
|
pos, tot_value);
|
|
}
|
|
|
|
move[pos].territorial_value = tot_value;
|
|
move[pos].secondary_value += secondary_value;
|
|
}
|
|
|
|
|
|
/*
|
|
* Estimate the strategical value of a move at (pos).
|
|
*/
|
|
static void
|
|
estimate_strategical_value(int pos, int color, float our_score,
|
|
int use_thrashing_dragon_heuristics)
|
|
{
|
|
int k;
|
|
int l;
|
|
int aa = NO_MOVE;
|
|
int bb = NO_MOVE;
|
|
float aa_value = 0.0;
|
|
float bb_value = 0.0;
|
|
|
|
float this_value = 0.0;
|
|
float tot_value = 0.0;
|
|
|
|
/* Strategical value of connecting or cutting dragons. */
|
|
float dragon_value[BOARDMAX];
|
|
|
|
for (aa = BOARDMIN; aa < BOARDMAX; aa++)
|
|
dragon_value[aa] = 0.0;
|
|
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos].reason[k];
|
|
if (r < 0)
|
|
break;
|
|
if (move_reasons[r].status & STRATEGICALLY_REDUNDANT)
|
|
continue;
|
|
|
|
this_value = 0.0;
|
|
switch (move_reasons[r].type) {
|
|
case ATTACK_MOVE:
|
|
case ATTACK_MOVE_GOOD_KO:
|
|
case ATTACK_MOVE_BAD_KO:
|
|
case DEFEND_MOVE:
|
|
case DEFEND_MOVE_GOOD_KO:
|
|
case DEFEND_MOVE_BAD_KO:
|
|
aa = move_reasons[r].what;
|
|
|
|
/* Defenseless stone */
|
|
if (worm[aa].defense_codes[0] == 0)
|
|
break;
|
|
|
|
if (doing_scoring && dragon[aa].status == DEAD)
|
|
break;
|
|
|
|
/* FIXME: This is totally ad hoc, just guessing the value of
|
|
* potential cutting points.
|
|
* FIXME: When worm[aa].cutstone2 == 1 we should probably add
|
|
* a followup value.
|
|
*/
|
|
if (worm[aa].cutstone2 > 1 && !worm[aa].inessential) {
|
|
double ko_factor = 1;
|
|
if (move_reasons[r].type == ATTACK_MOVE_GOOD_KO
|
|
|| move_reasons[r].type == DEFEND_MOVE_GOOD_KO) {
|
|
ko_factor = 0.6;
|
|
}
|
|
else if (move_reasons[r].type == ATTACK_MOVE_BAD_KO
|
|
|| move_reasons[r].type == DEFEND_MOVE_BAD_KO) {
|
|
ko_factor = 0.4;
|
|
}
|
|
this_value = 10.0 * (worm[aa].cutstone2 - 1) * ko_factor;
|
|
TRACE(" %1m: %f - %1m cutstone\n", pos, this_value, aa);
|
|
}
|
|
|
|
tot_value += this_value;
|
|
|
|
/* If the string is a lunch for a weak dragon, the attack or
|
|
* defense has a strategical value. This can be valued along
|
|
* the same lines as strategic_attack/strategic_defend.
|
|
*
|
|
* No points are awarded if the lunch is an inessential dragon
|
|
* or worm.
|
|
*/
|
|
if (DRAGON2(aa).safety == INESSENTIAL
|
|
|| worm[aa].inessential)
|
|
break;
|
|
|
|
/* If the lunch has no potential to create eyes, no points. */
|
|
if (max_lunch_eye_value(aa) == 0)
|
|
break;
|
|
|
|
/* Can't use k in this loop too. */
|
|
for (l = 0; l < next_lunch; l++)
|
|
if (lunch_worm[l] == aa) {
|
|
bb = lunch_dragon[l];
|
|
|
|
/* FIXME: This value cannot be computed without some measurement
|
|
* of how the actual move affects the dragon. The dragon safety
|
|
* alone is not enough. The question is whether the dragon is
|
|
* threatened or defended by the move or not.
|
|
*/
|
|
this_value = 1.8 * soft_cap(DRAGON2(bb).strategic_size, 15.0)
|
|
* dragon_weakness(bb, 0);
|
|
|
|
/* If this dragon consists of only one worm and that worm
|
|
* can be tactically captured or defended by this move, we
|
|
* have already counted the points as territorial value,
|
|
* unless it's assumed to be dead.
|
|
*/
|
|
if (dragon[bb].status != DEAD
|
|
&& dragon[bb].size == worm[bb].size
|
|
&& (attack_move_reason_known(pos, bb)
|
|
|| defense_move_reason_known(pos, bb)))
|
|
this_value = 0.0;
|
|
|
|
/* If this dragon can be tactically attacked and the move
|
|
* does not defend or attack, no points.
|
|
*/
|
|
if (worm[bb].attack_codes[0] != 0
|
|
&& ((color == board[bb] && !does_defend(pos, bb))
|
|
|| (color == OTHER_COLOR(board[bb])
|
|
&& !does_attack(pos, bb))))
|
|
this_value = 0.0;
|
|
|
|
/* If we are doing scoring, are alive, and the move loses
|
|
* territory, no points.
|
|
*/
|
|
if (doing_scoring
|
|
&& move[pos].territorial_value < 0.0
|
|
&& (DRAGON2(bb).safety == ALIVE
|
|
|| DRAGON2(bb).safety == STRONGLY_ALIVE
|
|
|| DRAGON2(bb).safety == INVINCIBLE))
|
|
this_value = 0.0;
|
|
|
|
if (this_value > dragon_value[bb]) {
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f - %1m attacked/defended\n",
|
|
pos, this_value, bb);
|
|
dragon_value[bb] = this_value;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case ATTACK_THREAT:
|
|
case DEFEND_THREAT:
|
|
break;
|
|
|
|
case EITHER_MOVE:
|
|
/* FIXME: Generalize this to more types of threats. */
|
|
/* FIXME: We need a policy if a move has several EITHER_MOVE
|
|
* reasons. Most likely not all of them can be achieved.
|
|
*/
|
|
aa = either_data[move_reasons[r].what].what1;
|
|
bb = either_data[move_reasons[r].what].what2;
|
|
|
|
/* If both worms are dead, this move reason has no value. */
|
|
if (dragon[aa].status == DEAD
|
|
&& dragon[bb].status == DEAD)
|
|
break;
|
|
|
|
/* Also if there is a combination attack, we assume it covers
|
|
* the same thing.
|
|
* FIXME: This is only applicable as long as the only moves
|
|
* handled by EITHER_MOVE are attacks.
|
|
*/
|
|
if (move_reason_known(pos, MY_ATARI_ATARI_MOVE, -1))
|
|
break;
|
|
|
|
aa_value = adjusted_worm_attack_value(pos, aa);
|
|
bb_value = adjusted_worm_attack_value(pos, bb);
|
|
this_value = gg_min(aa_value, bb_value);
|
|
|
|
TRACE(" %1m: %f - either attacks %1m (%f) or attacks %1m (%f)\n",
|
|
pos, this_value, aa, aa_value, bb, bb_value);
|
|
|
|
tot_value += this_value;
|
|
break;
|
|
|
|
case ALL_MOVE:
|
|
/* FIXME: Generalize this to more types of threats. */
|
|
aa = all_data[move_reasons[r].what].what1;
|
|
bb = all_data[move_reasons[r].what].what2;
|
|
|
|
/* If both worms are dead, this move reason has no value. */
|
|
if (dragon[aa].status == DEAD
|
|
&& dragon[bb].status == DEAD)
|
|
break;
|
|
|
|
/* Also if there is a combination attack, we assume it covers
|
|
* the same thing.
|
|
*/
|
|
if (move_reason_known(pos, YOUR_ATARI_ATARI_MOVE, -1))
|
|
break;
|
|
|
|
aa_value = worm[aa].effective_size;
|
|
bb_value = worm[bb].effective_size;
|
|
this_value = 2 * gg_min(aa_value, bb_value);
|
|
|
|
TRACE(" %1m: %f - both defends %1m (%f) and defends %1m (%f)\n",
|
|
pos, this_value, aa, aa_value, bb, bb_value);
|
|
|
|
tot_value += this_value;
|
|
break;
|
|
|
|
case CONNECT_MOVE:
|
|
|
|
/* If the opponent just added a stone to a dead dragon, which is
|
|
* adjacent to both dragons being connected, then the connection
|
|
* is probably a good way to make sure the thrashing dragon
|
|
* stays dead. If we are ahead, add a safety move here, at most
|
|
* half the margin of victory.
|
|
*
|
|
* This does only apply if we decided earlier we want to use
|
|
* thrashing dragon heuristics.
|
|
*/
|
|
|
|
if (use_thrashing_dragon_heuristics) {
|
|
int cc;
|
|
aa = dragon[conn_worm1[move_reasons[r].what]].origin;
|
|
bb = dragon[conn_worm2[move_reasons[r].what]].origin;
|
|
cc = get_last_opponent_move(color);
|
|
|
|
if (cc != NO_MOVE
|
|
&& thrashing_stone[cc]
|
|
&& are_neighbor_dragons(aa, cc)
|
|
&& are_neighbor_dragons(bb, cc)) {
|
|
if (aa == bb)
|
|
this_value = 1.6 * DRAGON2(cc).strategic_size;
|
|
else if (DRAGON2(aa).safety == INESSENTIAL
|
|
|| DRAGON2(bb).safety == INESSENTIAL) {
|
|
if ((DRAGON2(aa).safety == INESSENTIAL
|
|
&& max_lunch_eye_value(aa) == 0)
|
|
|| (DRAGON2(bb).safety == INESSENTIAL
|
|
&& max_lunch_eye_value(bb) == 0))
|
|
this_value = 0.0;
|
|
else
|
|
this_value = 0.8 * DRAGON2(cc).strategic_size;
|
|
}
|
|
else
|
|
this_value = 1.7 * DRAGON2(cc).strategic_size;
|
|
|
|
if (this_value > dragon_value[dragon[cc].origin]) {
|
|
dragon_value[dragon[cc].origin] = this_value;
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f - connect %1m and %1m to attack thrashing dragon %1m\n",
|
|
pos, this_value, aa, bb, cc);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!move[pos].move_safety)
|
|
break;
|
|
/* Otherwise fall through. */
|
|
case CUT_MOVE:
|
|
if (doing_scoring && !move[pos].move_safety)
|
|
break;
|
|
|
|
aa = dragon[conn_worm1[move_reasons[r].what]].origin;
|
|
bb = dragon[conn_worm2[move_reasons[r].what]].origin;
|
|
|
|
if (aa == bb)
|
|
continue;
|
|
|
|
/* If we are ahead by more than 20, value connections more strongly */
|
|
if (our_score > 20.0)
|
|
this_value = connection_value(aa, bb, pos, our_score);
|
|
else
|
|
this_value = connection_value(aa, bb, pos, 0);
|
|
if (this_value > dragon_value[aa]) {
|
|
dragon_value[aa] = this_value;
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f - %1m cut/connect strategic value\n",
|
|
pos, this_value, aa);
|
|
}
|
|
|
|
|
|
if (our_score > 20.0)
|
|
this_value = connection_value(bb, aa, pos, our_score);
|
|
else
|
|
this_value = connection_value(bb, aa, pos, 0);
|
|
if (this_value > dragon_value[bb]) {
|
|
dragon_value[bb] = this_value;
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f - %1m cut/connect strategic value\n",
|
|
pos, this_value, bb);
|
|
}
|
|
|
|
break;
|
|
|
|
case SEMEAI_MOVE:
|
|
/*
|
|
* The strategical value of winning a semeai is
|
|
* own dragons (usually) becomes fully secure, while adjoining
|
|
* enemy dragons do not.
|
|
*
|
|
* FIXME: Valuation not implemented at all yet.
|
|
*/
|
|
|
|
break;
|
|
|
|
case STRATEGIC_ATTACK_MOVE:
|
|
case STRATEGIC_DEFEND_MOVE:
|
|
/* The right way to do this is to estimate the safety of the
|
|
* dragon before and after the move. Unfortunately we are
|
|
* missing good ways to do this currently.
|
|
*
|
|
* Temporary solution is to only look at an ad hoc measure of
|
|
* the dragon safety and ignoring the effectiveness of the
|
|
* move.
|
|
*
|
|
* FIXME: Improve the implementation.
|
|
*/
|
|
aa = move_reasons[r].what;
|
|
|
|
/* FIXME: This value cannot be computed without some
|
|
* measurement of how the actual move affects the dragon. The
|
|
* dragon safety alone is not enough. The question is whether
|
|
* the dragon is threatened by the move or not.
|
|
*/
|
|
if (use_thrashing_dragon_heuristics
|
|
&& thrashing_stone[aa])
|
|
this_value = 1.7 * DRAGON2(aa).strategic_size;
|
|
else
|
|
this_value = 1.8 * soft_cap(DRAGON2(aa).strategic_size, 15.0)
|
|
* dragon_weakness(aa, 1);
|
|
|
|
/* No strategical attack value is awarded if the dragon at (aa)
|
|
* has an adjacent (friendly) critical dragon, which is not
|
|
* defended by this move. In that case it's probably a mistake
|
|
* to make the strategical attack since the dragon can defend
|
|
* itself with profit.
|
|
*
|
|
* FIXME: We probably need to verify that the critical dragon is
|
|
* substantial enough that capturing it saves the strategically
|
|
* attacked dragon.
|
|
*/
|
|
if (move_reasons[r].type == STRATEGIC_ATTACK_MOVE) {
|
|
int s;
|
|
|
|
for (s = 0; s < DRAGON2(aa).neighbors; s++) {
|
|
int d = DRAGON2(aa).adjacent[s];
|
|
int adj = dragon2[d].origin;
|
|
|
|
if (dragon[adj].color == color
|
|
&& dragon[adj].status == CRITICAL
|
|
&& dragon2[d].safety != INESSENTIAL
|
|
&& !owl_defense_move_reason_known(pos, adj))
|
|
this_value = 0.0;
|
|
}
|
|
}
|
|
|
|
/* Multiply by attack_dragon_weight to try to find a best fit */
|
|
this_value = this_value * attack_dragon_weight;
|
|
|
|
if (this_value > dragon_value[aa]) {
|
|
dragon_value[aa] = this_value;
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f - %1m strategic attack/defend\n",
|
|
pos, this_value, aa);
|
|
|
|
}
|
|
break;
|
|
|
|
case UNCERTAIN_OWL_DEFENSE:
|
|
aa = move_reasons[r].what;
|
|
|
|
/* If there is an adjacent dragon which is critical we should
|
|
* skip this type of move reason, since attacking or defending
|
|
* the critical dragon is more urgent.
|
|
*/
|
|
{
|
|
int d;
|
|
int found_one = 0;
|
|
|
|
for (d = 0; d < DRAGON2(aa).neighbors; d++)
|
|
if (DRAGON(DRAGON2(aa).adjacent[d]).status == CRITICAL)
|
|
found_one = 1;
|
|
if (found_one)
|
|
break;
|
|
}
|
|
|
|
/* If we are behind, we should skip this type of move reason.
|
|
* If we are ahead, we should value it more.
|
|
*/
|
|
if (our_score < 0.0)
|
|
this_value = 0.0;
|
|
else
|
|
this_value = gg_min(2*DRAGON2(aa).strategic_size, 0.65*our_score);
|
|
|
|
if (this_value > dragon_value[aa]) {
|
|
dragon_value[aa] = this_value;
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
" %1m: %f - %1m uncertain owl defense bonus\n",
|
|
pos, this_value, aa);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (aa = BOARDMIN; aa < BOARDMAX; aa++) {
|
|
if (dragon_value[aa] == 0.0)
|
|
continue;
|
|
|
|
ASSERT1(dragon[aa].origin == aa, aa);
|
|
|
|
/* If this dragon is critical but not attacked/defended by this
|
|
* move, we ignore the strategic effect.
|
|
*/
|
|
if (dragon[aa].status == CRITICAL
|
|
&& !owl_move_reason_known(pos, aa)) {
|
|
DEBUG(DEBUG_MOVE_REASONS, " %1m: 0.0 - disregarding strategic effect on %1m (critical dragon)\n",
|
|
pos, aa);
|
|
continue;
|
|
}
|
|
|
|
/* If this dragon consists of only one worm and that worm can
|
|
* be tactically captured or defended by this move, we have
|
|
* already counted the points as territorial value, unless
|
|
* it's assumed to be dead.
|
|
* However, we still allow strategical excess value (see below)
|
|
* in case the effective_size is substantially bigger (by 2.0)
|
|
* than the actualy size.
|
|
*/
|
|
if (dragon[aa].status != DEAD
|
|
&& dragon[aa].size == worm[aa].size
|
|
&& worm[aa].effective_size < worm[aa].size + 2.0
|
|
&& (attack_move_reason_known(pos, aa)
|
|
|| defense_move_reason_known(pos, aa))) {
|
|
TRACE(" %1m: %f - %1m strategic value already counted - A.\n",
|
|
pos, dragon_value[aa], aa);
|
|
continue;
|
|
}
|
|
/* If the dragon has been owl captured, owl defended, or involved
|
|
* in a semeai, we have likewise already counted the points as
|
|
* territorial value.
|
|
*/
|
|
if (attack_move_reason_known(pos, aa)
|
|
|| defense_move_reason_known(pos, aa)
|
|
|| (owl_move_reason_known(pos, aa)
|
|
&& dragon[aa].status == CRITICAL)
|
|
|| move_reason_known(pos, SEMEAI_MOVE, aa)) {
|
|
/* But if the strategical value was larger than the territorial
|
|
* value (e.g. because connecting to strong dragon) we award the
|
|
* excess value as a bonus.
|
|
*/
|
|
float excess_value = (dragon_value[aa] -
|
|
2 * DRAGON2(aa).strategic_size);
|
|
if (excess_value > 0.0) {
|
|
TRACE(" %1m: %f - strategic bonus for %1m\n", pos, excess_value, aa);
|
|
tot_value += excess_value;
|
|
}
|
|
else {
|
|
TRACE(" %1m: %f - %1m strategic value already counted - B.\n",
|
|
pos, dragon_value[aa], aa);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
TRACE(" %1m: %f - strategic effect on %1m\n",
|
|
pos, dragon_value[aa], aa);
|
|
tot_value += dragon_value[aa];
|
|
}
|
|
|
|
/* Finally, subtract penalty for invasion type moves. */
|
|
this_value = strategic_penalty(pos, color);
|
|
/* Multiply by invasion_malus_weight to allow us to fit the weight */
|
|
this_value = this_value * invasion_malus_weight;
|
|
if (this_value > 0.0) {
|
|
TRACE(" %1m: %f - strategic penalty, considered as invasion.\n",
|
|
pos, -this_value);
|
|
tot_value -= this_value;
|
|
}
|
|
|
|
move[pos].strategical_value = tot_value;
|
|
}
|
|
|
|
|
|
/* Compare two move reasons, used for sorting before presentation. */
|
|
static int
|
|
compare_move_reasons(const void *p1, const void *p2)
|
|
{
|
|
const int mr1 = *(const int *) p1;
|
|
const int mr2 = *(const int *) p2;
|
|
|
|
if (move_reasons[mr1].type != move_reasons[mr2].type)
|
|
return move_reasons[mr2].type - move_reasons[mr1].type;
|
|
else
|
|
return move_reasons[mr2].what - move_reasons[mr1].what;
|
|
}
|
|
|
|
|
|
/*
|
|
* Combine the reasons for a move at (pos) into a simple numerical value.
|
|
* These heuristics are now somewhat less ad hoc than before but probably
|
|
* still need a lot of improvement.
|
|
*/
|
|
static float
|
|
value_move_reasons(int pos, int color, float pure_threat_value,
|
|
float our_score, int use_thrashing_dragon_heuristics)
|
|
{
|
|
float tot_value;
|
|
float shape_factor;
|
|
|
|
gg_assert(stackp == 0);
|
|
|
|
/* Is it an antisuji? */
|
|
if (is_antisuji_move(pos))
|
|
return 0.0; /* This move must not be played. End of story. */
|
|
|
|
/* Never play on a vertex which is unconditional territory for
|
|
* either player. There is absolutely nothing to gain.
|
|
*/
|
|
if (worm[pos].unconditional_status != UNKNOWN)
|
|
return 0.0;
|
|
|
|
/* If this move has no reason at all, we can skip some steps. */
|
|
if (move[pos].reason[0] >= 0
|
|
|| move[pos].min_territory > 0.0) {
|
|
int num_reasons;
|
|
|
|
/* Sort the move reasons. This makes it easier to visually compare
|
|
* the reasons for different moves in the trace outputs.
|
|
*/
|
|
num_reasons = 0;
|
|
while (move[pos].reason[num_reasons] >= 0 && num_reasons < MAX_REASONS)
|
|
num_reasons++;
|
|
gg_sort(move[pos].reason, num_reasons, sizeof(move[pos].reason[0]),
|
|
compare_move_reasons);
|
|
|
|
/* Discard move reasons that only duplicate another. */
|
|
discard_redundant_move_reasons(pos);
|
|
|
|
/* Estimate the value of various aspects of the move. The order
|
|
* is significant. Territorial value must be computed before
|
|
* strategical value. See connection_value().
|
|
*/
|
|
estimate_territorial_value(pos, color, our_score, 0);
|
|
estimate_strategical_value(pos, color, our_score,
|
|
use_thrashing_dragon_heuristics);
|
|
}
|
|
|
|
/* Introduction of strategical_weight and territorial_weight,
|
|
* for automatic fitting. (3.5.1)
|
|
*/
|
|
tot_value = territorial_weight * move[pos].territorial_value +
|
|
strategical_weight * move[pos].strategical_value;
|
|
|
|
shape_factor = compute_shape_factor(pos);
|
|
|
|
if (tot_value > 0.0) {
|
|
int c;
|
|
float followup_value;
|
|
|
|
/* Negative territorial followup doesn't make make sense. */
|
|
if (move[pos].influence_followup_value < 0.0)
|
|
move[pos].influence_followup_value = 0.0;
|
|
|
|
followup_value = move[pos].followup_value
|
|
+ move[pos].influence_followup_value;
|
|
TRACE(" %1m: %f - total followup value, added %f as territorial followup\n",
|
|
pos, followup_value, move[pos].influence_followup_value);
|
|
|
|
/* In the endgame, there are a few situations where the value can
|
|
* be 0 points + followup. But we want to take the intersections first
|
|
* were we actually get some points. 0.5 points is a 1 point ko which
|
|
* is the smallest value that is actually worth something.
|
|
*/
|
|
if (tot_value >= 0.5) {
|
|
float old_tot_value = tot_value;
|
|
float contribution;
|
|
/* We adjust the value according to followup and reverse followup
|
|
* values.
|
|
*/
|
|
contribution = gg_min(gg_min(0.5 * followup_value
|
|
+ 0.5 * move[pos].reverse_followup_value,
|
|
1.0 * tot_value
|
|
+ followup_value),
|
|
1.1 * tot_value
|
|
+ move[pos].reverse_followup_value);
|
|
tot_value += contribution * followup_weight;
|
|
/* The first case applies to gote vs gote situation, the
|
|
* second to reverse sente, and the third to sente situations.
|
|
* The usual rule is that a sente move should count at double
|
|
* value. But if we have a 1 point move with big followup (i.e.
|
|
* sente) we want to play that before a 2 point gote move. Hence
|
|
* the factor 1.1 above.
|
|
*/
|
|
|
|
if (contribution != 0.0) {
|
|
TRACE(" %1m: %f - added due to followup (%f) and reverse followup values (%f)\n",
|
|
pos, contribution, followup_value,
|
|
move[pos].reverse_followup_value);
|
|
}
|
|
|
|
/* If a ko fight is going on, we should use the full followup
|
|
* and reverse followup values in the total value. We save the
|
|
* additional contribution for later access.
|
|
*/
|
|
move[pos].additional_ko_value =
|
|
followup_value
|
|
+ move[pos].reverse_followup_value
|
|
- (tot_value - old_tot_value);
|
|
|
|
/* Not sure whether this could happen, but check for safety. */
|
|
if (move[pos].additional_ko_value < 0.0)
|
|
move[pos].additional_ko_value = 0.0;
|
|
}
|
|
else {
|
|
move[pos].additional_ko_value =
|
|
shape_factor * (move[pos].followup_value
|
|
+ move[pos].reverse_followup_value);
|
|
}
|
|
|
|
tot_value += soft_cap(0.05 * move[pos].secondary_value, 0.4);
|
|
if (move[pos].secondary_value != 0.0)
|
|
TRACE(" %1m: %f - secondary\n", pos,
|
|
soft_cap(0.05 * move[pos].secondary_value, 0.4));
|
|
|
|
if (move[pos].numpos_shape + move[pos].numneg_shape > 0) {
|
|
/* shape_factor has already been computed. */
|
|
float old_value = tot_value;
|
|
/* Maximum 15 points of the territorial value will be weighted by shape_factor */
|
|
if (move[pos].territorial_value < 15)
|
|
tot_value *= shape_factor;
|
|
else {
|
|
float non_shape_val = move[pos].territorial_value - 15;
|
|
tot_value = (tot_value - non_shape_val) * shape_factor + non_shape_val;
|
|
}
|
|
|
|
if (verbose) {
|
|
/* Should all have been TRACE, except we want field sizes. */
|
|
gprintf(" %1m: %f - shape ", pos, tot_value - old_value);
|
|
fprintf(stderr,
|
|
"(shape values +%4.2f(%d) -%4.2f(%d), shape factor %5.3f)\n",
|
|
move[pos].maxpos_shape, move[pos].numpos_shape,
|
|
move[pos].maxneg_shape, move[pos].numneg_shape,
|
|
shape_factor);
|
|
}
|
|
}
|
|
|
|
/* Add a special shape bonus for moves which connect own strings
|
|
* or cut opponent strings.
|
|
*/
|
|
c = (move_connects_strings(pos, color, 1)
|
|
+ move_connects_strings(pos, OTHER_COLOR(color), 0));
|
|
if (c > 0) {
|
|
float shape_factor2 = pow(1.02, (float) c) - 1;
|
|
float base_value = gg_max(gg_min(tot_value, 5.0), 1.0);
|
|
if (verbose) {
|
|
/* Should all have been TRACE, except we want field sizes. */
|
|
gprintf(" %1m: %f - connects strings ", pos,
|
|
base_value * shape_factor2);
|
|
fprintf(stderr, "(connect value %d, shape factor %5.3f)\n", c,
|
|
shape_factor2);
|
|
}
|
|
tot_value += base_value * shape_factor2;
|
|
}
|
|
|
|
/* Dame points which have a cut or connect move reason get a small
|
|
* extra bonus because these have a tendency to actually be worth
|
|
* a point.
|
|
*/
|
|
if (tot_value < 0.3
|
|
&& (move_reason_known(pos, CONNECT_MOVE, -1)
|
|
|| move_reason_known(pos, CUT_MOVE, -1))) {
|
|
float old_tot_value = tot_value;
|
|
tot_value = gg_min(0.3, tot_value + 0.1);
|
|
TRACE(" %1m: %f - cut/connect dame bonus\n", pos,
|
|
tot_value - old_tot_value);
|
|
}
|
|
}
|
|
else {
|
|
move[pos].additional_ko_value =
|
|
shape_factor * (move[pos].followup_value +
|
|
gg_min(move[pos].followup_value,
|
|
move[pos].reverse_followup_value));
|
|
}
|
|
|
|
/* If the move is valued 0 or small, but has followup values and is
|
|
* flagged as a worthwhile threat, add up to pure_threat_value to
|
|
* the move.
|
|
*
|
|
* FIXME: We shouldn't have to call confirm_safety() here. It's
|
|
* potentially too expensive.
|
|
*/
|
|
if (pure_threat_value > 0.0
|
|
&& move[pos].worthwhile_threat
|
|
&& tot_value <= pure_threat_value
|
|
&& board[pos] == EMPTY
|
|
&& move[pos].additional_ko_value > 0.0
|
|
&& is_legal(pos, color)
|
|
&& value_moves_confirm_safety(pos, color)) {
|
|
float new_tot_value = gg_min(pure_threat_value,
|
|
tot_value
|
|
+ 0.25 * move[pos].additional_ko_value);
|
|
|
|
/* Make sure that moves with independent value are preferred over
|
|
* those without.
|
|
*/
|
|
new_tot_value *= (1.0 - 0.1 * (pure_threat_value - tot_value)
|
|
/ pure_threat_value);
|
|
|
|
if (new_tot_value > tot_value) {
|
|
TRACE(" %1m: %f - carry out threat or defend against threat\n",
|
|
pos, new_tot_value - tot_value);
|
|
tot_value = new_tot_value;
|
|
}
|
|
}
|
|
|
|
/* min_value is now subject to reduction with a fitted weight (3.5.1) */
|
|
move[pos].min_value = move[pos].min_value * minimum_value_weight;
|
|
move[pos].max_value = move[pos].max_value * maximum_value_weight;
|
|
|
|
/* Test if min_value or max_value values constrain the total value.
|
|
* First avoid contradictions between min_value and max_value,
|
|
* assuming that min_value is right.
|
|
*/
|
|
if (move[pos].min_value > move[pos].max_value)
|
|
move[pos].max_value = move[pos].min_value;
|
|
|
|
/* If several moves have an identical minimum value, then GNU Go uses the
|
|
* following secondary criterion (unless min_value and max_value agree, and
|
|
* unless min_value is bigger than 25, in which case it probably comes from
|
|
* a J or U pattern):
|
|
*/
|
|
if (move[pos].min_value < 25)
|
|
move[pos].min_value += tot_value / 200;
|
|
if (tot_value < move[pos].min_value
|
|
&& move[pos].min_value > 0) {
|
|
tot_value = move[pos].min_value;
|
|
TRACE(" %1m: %f - minimum accepted value\n", pos, tot_value);
|
|
}
|
|
|
|
if (tot_value > move[pos].max_value) {
|
|
tot_value = move[pos].max_value;
|
|
TRACE(" %1m: %f - maximum accepted value\n",
|
|
pos, tot_value);
|
|
}
|
|
|
|
if (tot_value > 0
|
|
|| move[pos].territorial_value > 0
|
|
|| move[pos].strategical_value > 0) {
|
|
TRACE("Move generation values %1m to %f\n", pos, tot_value);
|
|
move_considered(pos, tot_value);
|
|
}
|
|
|
|
return tot_value;
|
|
}
|
|
|
|
|
|
/*
|
|
* Loop over all possible moves and value the move reasons for each.
|
|
*/
|
|
static void
|
|
value_moves(int color, float pure_threat_value, float our_score,
|
|
int use_thrashing_dragon_heuristics)
|
|
{
|
|
int m, n;
|
|
int pos;
|
|
|
|
TRACE("\nMove valuation:\n");
|
|
|
|
/* Visit the moves in the standard lexicographical order */
|
|
for (n = 0; n < board_size; n++)
|
|
for (m = board_size-1; m >= 0; m--) {
|
|
pos = POS(m, n);
|
|
|
|
move[pos].value = value_move_reasons(pos, color,
|
|
pure_threat_value, our_score,
|
|
use_thrashing_dragon_heuristics);
|
|
if (move[pos].value == 0.0)
|
|
continue;
|
|
|
|
/* Maybe this test should be performed elsewhere. This is just
|
|
* to get some extra safety. We don't filter out illegal ko
|
|
* captures here though, because if that is the best move, we
|
|
* should reevaluate ko threats.
|
|
*/
|
|
if (is_legal(pos, color) || is_illegal_ko_capture(pos, color)) {
|
|
/* Add a random number between 0 and 0.01 to use in comparisons. */
|
|
move[pos].value +=
|
|
0.01 * move[pos].random_number * move[pos].randomness_scaling;
|
|
}
|
|
else {
|
|
move[pos].value = 0.0;
|
|
TRACE("Move at %1m wasn't legal.\n", pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Print the values of all moves with values bigger than zero. */
|
|
|
|
void
|
|
print_all_move_values(FILE *output)
|
|
{
|
|
int pos;
|
|
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
if (!ON_BOARD(pos) || move[pos].final_value <= 0.0)
|
|
continue;
|
|
|
|
gfprintf(output, "%1M %f\n", pos, move[pos].final_value);
|
|
}
|
|
}
|
|
|
|
/* Search through all board positions for the 10 highest valued
|
|
* moves and print them.
|
|
*/
|
|
|
|
static void
|
|
print_top_moves(void)
|
|
{
|
|
int k;
|
|
int pos;
|
|
float tval;
|
|
|
|
for (k = 0; k < 10; k++) {
|
|
best_moves[k] = NO_MOVE;
|
|
best_move_values[k] = 0.0;
|
|
}
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
if (!ON_BOARD(pos) || move[pos].final_value <= 0.0)
|
|
continue;
|
|
|
|
tval = move[pos].final_value;
|
|
record_top_move(pos, tval);
|
|
}
|
|
|
|
if (verbose > 0 || (debug & DEBUG_TOP_MOVES)) {
|
|
gprintf("\nTop moves:\n");
|
|
for (k = 0; k < 10 && best_move_values[k] > 0.0; k++)
|
|
gprintf("%d. %1M %f\n", k+1, best_moves[k], best_move_values[k]);
|
|
}
|
|
}
|
|
|
|
/* Add a move to the list of top moves (if it is among the top ten) */
|
|
|
|
void
|
|
record_top_move(int pos, float val)
|
|
{
|
|
int k;
|
|
for (k = 9; k >= 0; k--)
|
|
if (val > best_move_values[k]) {
|
|
if (k < 9) {
|
|
best_move_values[k+1] = best_move_values[k];
|
|
best_moves[k+1] = best_moves[k];
|
|
}
|
|
best_move_values[k] = val;
|
|
best_moves[k] = pos;
|
|
}
|
|
|
|
move[pos].final_value = val;
|
|
}
|
|
|
|
/* remove a rejected move from the list of top moves */
|
|
|
|
void
|
|
remove_top_move(int move)
|
|
{
|
|
int k;
|
|
for (k = 0; k < 10; k++) {
|
|
if (best_moves[k] == move) {
|
|
int l;
|
|
for (l = k; l < 9; l++) {
|
|
best_moves[l] = best_moves[l+1];
|
|
best_move_values[l] = best_move_values[l+1];
|
|
}
|
|
best_moves[9] = NO_MOVE;
|
|
best_move_values[9] = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* This function is called if the biggest move on board was an illegal
|
|
* ko capture.
|
|
*/
|
|
static void
|
|
reevaluate_ko_threats(int ko_move, int color, float ko_value)
|
|
{
|
|
int ko_stone = NO_MOVE;
|
|
int opp_ko_move;
|
|
int pos;
|
|
int k;
|
|
int type, what;
|
|
int threat_does_work = 0;
|
|
int ko_move_target;
|
|
int num_good_threats = 0;
|
|
int good_threats[BOARDMAX];
|
|
int best_threat_quality = -1;
|
|
float threat_size;
|
|
|
|
ko_move_target = get_biggest_owl_target(ko_move);
|
|
|
|
/* If the move is a simple ko recapture, find the ko stone. (If
|
|
* it's not a simple ko recapture, then the move must be a superko
|
|
* violation.)
|
|
*/
|
|
if (is_illegal_ko_capture(ko_move, color)) {
|
|
for (k = 0; k <= 3; k++) {
|
|
ko_stone = ko_move + delta[k];
|
|
if (ON_BOARD(ko_stone) && countlib(ko_stone) == 1)
|
|
break;
|
|
}
|
|
ASSERT_ON_BOARD1(ko_stone);
|
|
}
|
|
|
|
TRACE("Reevaluating ko threats.\n");
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
int threat_quality = 0;
|
|
|
|
if (!ON_BOARD(pos) || pos == ko_move)
|
|
continue;
|
|
if (move[pos].additional_ko_value <= 0.0)
|
|
continue;
|
|
|
|
/* Otherwise we look for the biggest threat, and then check whether
|
|
* it still works after ko has been resolved.
|
|
*/
|
|
|
|
/* `additional_ko_value' includes reverse followup. While it is good to
|
|
* play ko threats which eliminate other threats in turn, we should
|
|
* always prefer threats that are larger than the value of the ko.
|
|
*/
|
|
if (move[pos].followup_value < ko_value)
|
|
threat_quality = -1;
|
|
|
|
threat_size = 0.0;
|
|
type = -1;
|
|
what = -1;
|
|
for (k = 0; k < MAX_REASONS; k++) {
|
|
int r = move[pos].reason[k];
|
|
if (r < 0)
|
|
break;
|
|
if (!(move_reasons[r].type & THREAT_BIT))
|
|
continue;
|
|
|
|
switch (move_reasons[r].type) {
|
|
case ATTACK_THREAT:
|
|
case DEFEND_THREAT:
|
|
if (worm[move_reasons[r].what].effective_size
|
|
> threat_size) {
|
|
threat_size = worm[move_reasons[r].what].effective_size;
|
|
type = move_reasons[r].type;
|
|
what = move_reasons[r].what;
|
|
}
|
|
break;
|
|
case OWL_ATTACK_THREAT:
|
|
case OWL_DEFEND_THREAT:
|
|
case SEMEAI_THREAT:
|
|
if (dragon[move_reasons[r].what].effective_size
|
|
> threat_size) {
|
|
threat_size = dragon[move_reasons[r].what]\
|
|
.effective_size;
|
|
type = move_reasons[r].type;
|
|
what = move_reasons[r].what;
|
|
}
|
|
break;
|
|
default:
|
|
/* This means probably someone has introduced a new threat type
|
|
* without adding the corresponding case above.
|
|
*/
|
|
gg_assert(0);
|
|
break;
|
|
}
|
|
}
|
|
/* If there is no threat recorded, the followup value is probably
|
|
* contributed by a pattern. We can do nothing but accept this value.
|
|
* (although this does cause problems).
|
|
*
|
|
* FIXME: In the case of superko violation we have no ko_stone.
|
|
* Presumably some of the tests below should be applicable anyway.
|
|
* Currently we just say that any threat is ok.
|
|
*/
|
|
if (type == -1 || ko_stone == NO_MOVE)
|
|
threat_does_work = 1;
|
|
else {
|
|
if (trymove(pos, color, "reevaluate_ko_threats", ko_move)) {
|
|
ASSERT_ON_BOARD1(ko_stone);
|
|
if (!find_defense(ko_stone, &opp_ko_move))
|
|
threat_does_work = 1;
|
|
else {
|
|
int threat_wastes_point = 0;
|
|
if (whose_area(OPPOSITE_INFLUENCE(color), pos) != EMPTY)
|
|
threat_wastes_point = 1;
|
|
|
|
if (trymove(opp_ko_move, OTHER_COLOR(color),
|
|
"reevaluate_ko_threats", ko_move)) {
|
|
switch (type) {
|
|
case ATTACK_THREAT:
|
|
/* In case the attack threat was a snapback move, there
|
|
* is no stone on the board to attack now and we check
|
|
* for a defense of the threatening move instead.
|
|
*/
|
|
if (board[what] != EMPTY)
|
|
threat_does_work = attack(what, NULL);
|
|
else
|
|
threat_does_work = find_defense(pos, NULL);
|
|
break;
|
|
case DEFEND_THREAT:
|
|
threat_does_work = (board[what] != EMPTY
|
|
&& find_defense(what, NULL));
|
|
break;
|
|
case OWL_ATTACK_THREAT:
|
|
case OWL_DEFEND_THREAT:
|
|
/* Should we call do_owl_attack/defense here?
|
|
* Maybe too expensive? For the moment we just assume
|
|
* that the attack does not work if it concerns the
|
|
* same dragon as ko_move. (Can this really happen?)
|
|
*/
|
|
threat_does_work = (ko_move_target != what);
|
|
}
|
|
popgo();
|
|
|
|
/* Is this a losing ko threat? */
|
|
if (threat_does_work && type == ATTACK_THREAT) {
|
|
int apos;
|
|
if (attack(pos, &apos)
|
|
&& does_defend(apos, what)
|
|
&& (forced_backfilling_moves[apos]
|
|
|| (!is_proper_eye_space(apos)
|
|
&& !false_eye_territory[apos]))) {
|
|
threat_does_work = 0;
|
|
}
|
|
}
|
|
|
|
/* If we are fighting a tiny ko (1 - 2 points only), we pay
|
|
* extra attention to select threats that don't waste points.
|
|
* In particular, we don't play threats inside of opponent
|
|
* territory if they can be averted on a dame intersection.
|
|
*/
|
|
if (ko_value < 1.0
|
|
&& threat_does_work
|
|
&& threat_quality >= 0
|
|
&& (type == ATTACK_THREAT || type == DEFEND_THREAT)) {
|
|
int averting_pos;
|
|
|
|
if (type == ATTACK_THREAT)
|
|
find_defense(what, &averting_pos);
|
|
else
|
|
attack(what, &averting_pos);
|
|
|
|
/* `averting_pos' can be NO_MOVE sometimes, at least when
|
|
* when the the threat is a threat to attack. It is not
|
|
* clear what to do in such cases.
|
|
*/
|
|
if (averting_pos != NO_MOVE) {
|
|
int averting_wastes_point = 0;
|
|
if (whose_territory(OPPOSITE_INFLUENCE(color), averting_pos)
|
|
!= EMPTY)
|
|
averting_wastes_point = 1;
|
|
threat_quality = averting_wastes_point - threat_wastes_point;
|
|
if (threat_quality < 0)
|
|
threat_does_work = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
popgo();
|
|
}
|
|
}
|
|
|
|
if (threat_does_work) {
|
|
if (threat_quality == best_threat_quality)
|
|
good_threats[num_good_threats++] = pos;
|
|
else if (threat_quality > best_threat_quality) {
|
|
best_threat_quality = threat_quality;
|
|
num_good_threats = 0;
|
|
good_threats[num_good_threats++] = pos;
|
|
}
|
|
else
|
|
DEBUG(DEBUG_MOVE_REASONS,
|
|
"%1m: no additional ko value (threat does not work as ko threat)\n", pos);
|
|
}
|
|
}
|
|
|
|
for (k = 0; k < num_good_threats; k++) {
|
|
pos = good_threats[k];
|
|
|
|
/* If the move previously had no value, we need to add in the
|
|
* randomness contribution now.
|
|
*
|
|
* FIXME: This is very ugly. Restructure the code so that the
|
|
* randomness need only be considered in one place.
|
|
*/
|
|
if (move[pos].value == 0.0) {
|
|
move[pos].value +=
|
|
0.01 * move[pos].random_number * move[pos].randomness_scaling;
|
|
}
|
|
|
|
TRACE("%1m: %f + %f = %f\n", pos, move[pos].value,
|
|
move[pos].additional_ko_value,
|
|
move[pos].value + move[pos].additional_ko_value);
|
|
move[pos].value += move[pos].additional_ko_value;
|
|
}
|
|
}
|
|
|
|
|
|
/* Redistribute points. When one move is declared a replacement for
|
|
* another by a replacement move reason, the move values for the
|
|
* inferior move are transferred to the replacement.
|
|
*/
|
|
static void
|
|
redistribute_points(void)
|
|
{
|
|
int source;
|
|
int target;
|
|
|
|
for (target = BOARDMIN; target < BOARDMAX; target++)
|
|
if (ON_BOARD(target))
|
|
move[target].final_value = move[target].value;
|
|
|
|
for (source = BOARDMIN; source < BOARDMAX; source++) {
|
|
if (!ON_BOARD(source))
|
|
continue;
|
|
target = replacement_map[source];
|
|
if (target == NO_MOVE)
|
|
continue;
|
|
|
|
TRACE("Redistributing points from %1m to %1m.\n", source, target);
|
|
if (move[target].final_value < move[source].final_value) {
|
|
TRACE("%1m is now valued %f.\n", target, move[source].final_value);
|
|
move[target].final_value = move[source].final_value;
|
|
}
|
|
TRACE("%1m is now valued 0.\n", source);
|
|
move[source].final_value = 0.0;
|
|
}
|
|
}
|
|
|
|
/* This selects the best move available according to their valuations.
|
|
* If the best move is an illegal ko capture, we add ko threat values.
|
|
* If the best move is a blunder, it gets devalued and continue to look
|
|
* for the best move.
|
|
*/
|
|
static int
|
|
find_best_move(int *the_move, float *value, int color,
|
|
int allowed_moves[BOARDMAX])
|
|
{
|
|
int good_move_found = 0;
|
|
signed char blunder_tested[BOARDMAX];
|
|
float best_value = 0.0;
|
|
int best_move = NO_MOVE;
|
|
int pos;
|
|
|
|
memset(blunder_tested, 0, sizeof(blunder_tested));
|
|
|
|
while (!good_move_found) {
|
|
best_value = 0.0;
|
|
best_move = NO_MOVE;
|
|
|
|
/* Search through all board positions for the highest valued move. */
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
float this_value = move[pos].final_value;
|
|
if (allowed_moves && !allowed_moves[pos])
|
|
continue;
|
|
if (!ON_BOARD(pos) || move[pos].final_value == 0.0)
|
|
continue;
|
|
|
|
if (this_value > best_value) {
|
|
if (is_legal(pos, color) || is_illegal_ko_capture(pos, color)) {
|
|
best_value = this_value;
|
|
best_move = pos;
|
|
}
|
|
else {
|
|
TRACE("Move at %1m would be suicide.\n", pos);
|
|
remove_top_move(pos);
|
|
move[pos].value = 0.0;
|
|
move[pos].final_value = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If the best move is an illegal ko capture, reevaluate ko
|
|
* threats and search again.
|
|
*/
|
|
if (best_value > 0.0
|
|
&& (is_illegal_ko_capture(best_move, color)
|
|
|| !is_allowed_move(best_move, color))) {
|
|
TRACE("Move at %1m would be an illegal ko capture.\n", best_move);
|
|
reevaluate_ko_threats(best_move, color, best_value);
|
|
redistribute_points();
|
|
time_report(2, " reevaluate_ko_threats", NO_MOVE, 1.0);
|
|
remove_top_move(best_move);
|
|
move[best_move].value = 0.0;
|
|
move[best_move].final_value = 0.0;
|
|
print_top_moves();
|
|
good_move_found = 0;
|
|
}
|
|
/* Call blunder_size() to check that we're not about to make a
|
|
* blunder. Otherwise devalue this move and scan through all move
|
|
* values once more.
|
|
*/
|
|
else if (best_value > 0.0) {
|
|
if (!blunder_tested[best_move]) {
|
|
float blunder_size = value_moves_get_blunder_size(best_move, color);
|
|
if (blunder_size > 0.0) {
|
|
TRACE("Move at %1m is a blunder, subtracting %f.\n", best_move,
|
|
blunder_size);
|
|
remove_top_move(best_move);
|
|
move[best_move].value -= blunder_size;
|
|
move[best_move].final_value -= blunder_size;
|
|
TRACE("Move at %1m is now valued %f.\n", best_move,
|
|
move[best_move].final_value);
|
|
record_top_move(best_move, move[best_move].final_value);
|
|
good_move_found = 0;
|
|
blunder_tested[best_move] = 1;
|
|
}
|
|
else
|
|
good_move_found = 1; /* Best move was not a blunder. */
|
|
}
|
|
else /* The move apparently was a blunder, but still the best move. */
|
|
good_move_found = 1;
|
|
}
|
|
else
|
|
good_move_found = 1; /* It's best to pass. */
|
|
}
|
|
|
|
if (best_value > 0.0
|
|
&& best_move != NO_MOVE) {
|
|
*the_move = best_move;
|
|
*value = best_value;
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* Review the move reasons to find which (if any) move we want to play.
|
|
*
|
|
* The parameter pure_threat_value is the value assigned to a move
|
|
* which only threatens to capture or kill something. The reason for
|
|
* playing these is that the move may be effective because we have
|
|
* misevaluated the dangers or because the opponent misplays.
|
|
*
|
|
* The array allowed_moves restricts which moves may be considered. If
|
|
* NULL any move is allowed.
|
|
*/
|
|
int
|
|
review_move_reasons(int *the_move, float *value, int color,
|
|
float pure_threat_value, float our_score,
|
|
int allowed_moves[BOARDMAX],
|
|
int use_thrashing_dragon_heuristics)
|
|
{
|
|
int save_verbose;
|
|
|
|
current_color = color;
|
|
|
|
start_timer(2);
|
|
find_more_attack_and_defense_moves(color);
|
|
time_report(2, " find_more_attack_and_defense_moves", NO_MOVE, 1.0);
|
|
|
|
if (get_level() >= 6) {
|
|
find_more_owl_attack_and_defense_moves(color);
|
|
time_report(2, " find_more_owl_attack_and_defense_moves", NO_MOVE, 1.0);
|
|
}
|
|
|
|
if (large_scale && get_level() >= 6) {
|
|
find_large_scale_owl_attack_moves(color);
|
|
time_report(2, " find_large_scale_owl_attack_moves", NO_MOVE, 1.0);
|
|
}
|
|
|
|
find_more_semeai_moves(color);
|
|
time_report(2, " find_more_semeai_moves", NO_MOVE, 1.0);
|
|
|
|
save_verbose = verbose;
|
|
if (verbose > 0)
|
|
verbose--;
|
|
examine_move_safety(color);
|
|
time_report(2, " examine_move_safety", NO_MOVE, 1.0);
|
|
verbose = save_verbose;
|
|
|
|
/* We can't do this until move_safety is known. */
|
|
induce_secondary_move_reasons(color);
|
|
time_report(2, " induce_secondary_move_reasons", NO_MOVE, 1.0);
|
|
|
|
if (printworms || verbose)
|
|
list_move_reasons(stderr, NO_MOVE);
|
|
|
|
/* Evaluate all moves with move reasons. */
|
|
value_moves(color, pure_threat_value, our_score,
|
|
use_thrashing_dragon_heuristics);
|
|
time_report(2, " value_moves", NO_MOVE, 1.0);
|
|
|
|
/* Perform point redistribution */
|
|
redistribute_points();
|
|
|
|
/* Search through all board positions for the 10 highest valued
|
|
* moves and print them.
|
|
*/
|
|
print_top_moves();
|
|
|
|
/* Select the highest valued move and return it. */
|
|
return find_best_move(the_move, value, color, allowed_moves);
|
|
}
|
|
|
|
|
|
/*
|
|
* Choosing a strategy based on the current score estimate
|
|
* and the game status (between 0.0 (start) and 1.0 (game over)).
|
|
*/
|
|
|
|
void
|
|
choose_strategy(int color, float our_score, float game_status)
|
|
{
|
|
|
|
minimum_value_weight = 1.0;
|
|
maximum_value_weight = 1.0;
|
|
territorial_weight = 1.0;
|
|
strategical_weight = 1.0;
|
|
attack_dragon_weight = 1.0;
|
|
invasion_malus_weight = 1.0;
|
|
followup_weight = 1.0;
|
|
|
|
TRACE(" Game status = %f (0.0 = start, 1.0 = game over)\n", game_status);
|
|
|
|
|
|
if (cosmic_gnugo) {
|
|
|
|
if (game_status > 0.65 && our_score > 15.0) {
|
|
|
|
/* We seem to be winning, so we use conservative settings. */
|
|
minimum_value_weight = 0.66;
|
|
maximum_value_weight = 2.0;
|
|
territorial_weight = 0.95;
|
|
strategical_weight = 1.0;
|
|
attack_dragon_weight = 1.1;
|
|
invasion_malus_weight = 1.3;
|
|
followup_weight = 1.1;
|
|
TRACE(" %s is leading, using conservative settings.\n",
|
|
color == WHITE ? "White" : "Black");
|
|
}
|
|
else if (game_status > 0.16) {
|
|
|
|
/* We're not winning enough yet, try aggressive settings. */
|
|
minimum_value_weight = 0.66;
|
|
maximum_value_weight = 2.0;
|
|
territorial_weight = 1.4;
|
|
strategical_weight = 0.5;
|
|
attack_dragon_weight = 0.62;
|
|
invasion_malus_weight = 2.0;
|
|
followup_weight = 0.62;
|
|
|
|
/* If we're getting desesperate, try invasions as a last resort */
|
|
if (game_status > 0.75 && our_score < -25.0)
|
|
invasion_malus_weight = 0.2;
|
|
|
|
TRACE(" %s is not winning enough, using aggressive settings.\n",
|
|
color == WHITE ? "White" : "Black");
|
|
}
|
|
}
|
|
}
|
|
|
|
/* In order to get valid influence data after a move, we need to rerun
|
|
* estimate_territorial_value() for that move. A prerequisite for
|
|
* using this function is that move reasons have already been collected.
|
|
*
|
|
* This function should only be used for debugging purposes.
|
|
*/
|
|
void
|
|
prepare_move_influence_debugging(int pos, int color)
|
|
{
|
|
float our_score;
|
|
|
|
if (color == WHITE)
|
|
our_score = black_score;
|
|
else
|
|
our_score = -white_score;
|
|
|
|
estimate_territorial_value(pos, color, our_score, 1);
|
|
}
|
|
|
|
|
|
/* Compute probabilities of each move being played. It is assumed
|
|
* that the `move[]' array is filled with proper values (i.e. that
|
|
* one of the genmove*() functions has been called).
|
|
*
|
|
* The value of each move `V_k' should be a uniformly distributed
|
|
* random variable (`k' is a unique move index). Let it have values
|
|
* from the interval [l_k; u_k] . Then move value has constant
|
|
* probability density on the interval:
|
|
*
|
|
* 1
|
|
* d_k = -----------.
|
|
* u_k - l_k
|
|
*
|
|
* We need to determine the probability of `V_k' being the largest of
|
|
* {V_1, V_2, ..., V_n}. Probability density is like follows:
|
|
*
|
|
* D_k(t) = d_k * Product(P{V_i < t} for i != k), l_k <= t <= u_k,
|
|
*
|
|
* where P{A} is the probability of event `A'. By integrating D_k(t)
|
|
* from `l_k' to `u_k' we can find the probability in question:
|
|
*
|
|
* P{V_k > V_i for i != k} = Integrate(D_k(t) dt from l_k to u_k).
|
|
*
|
|
* Function D_k(t) is a polynomial on each of subintervals produced by
|
|
* points `l_k', `u_k', k = 1, ..., n. When t < min(l_k), D_k(t) is
|
|
* zero. On other subintervals it can be evaluated by taking into
|
|
* account that
|
|
*
|
|
* P{V_i < t} = d_i * (t - l_i) if t < u_i;
|
|
* P{V_i < t} = 1 if t >= u_i.
|
|
*/
|
|
void
|
|
compute_move_probabilities(float probabilities[BOARDMAX])
|
|
{
|
|
int k;
|
|
int pos;
|
|
int num_moves = 0;
|
|
int moves[BOARDMAX];
|
|
double lower_values[BOARDMAX];
|
|
double upper_values[BOARDMAX];
|
|
double densities[BOARDMAX];
|
|
double common_lower_limit = 0.0;
|
|
|
|
/* Find all moves with positive values. */
|
|
for (pos = BOARDMIN; pos < BOARDMAX; pos++) {
|
|
probabilities[pos] = 0.0;
|
|
|
|
if (ON_BOARD(pos)) {
|
|
/* FIXME: what about point redistribution? */
|
|
if (move[pos].final_value > 0.0) {
|
|
double scale = 0.01 * (double) move[pos].randomness_scaling;
|
|
|
|
moves[num_moves] = pos;
|
|
lower_values[num_moves] = ((double) move[pos].final_value
|
|
- (scale * move[pos].random_number));
|
|
upper_values[num_moves] = lower_values[num_moves] + scale;
|
|
densities[num_moves] = 1.0 / scale;
|
|
|
|
if (lower_values[num_moves] > common_lower_limit)
|
|
common_lower_limit = lower_values[num_moves];
|
|
|
|
num_moves++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Compute probability of each move. */
|
|
for (k = 0; k < num_moves; k++) {
|
|
int i;
|
|
double lower_limit = common_lower_limit;
|
|
|
|
/* Iterate over subintervals for integration. */
|
|
while (lower_limit < upper_values[k]) {
|
|
int j;
|
|
double upper_limit = upper_values[k];
|
|
double span_power;
|
|
double polynomial[BOARDMAX];
|
|
int degree;
|
|
|
|
degree = 0;
|
|
polynomial[0] = 1.0;
|
|
|
|
for (i = 0; i < num_moves; i++) {
|
|
/* See if we need to decrease current subinterval. */
|
|
if (upper_values[i] > lower_limit && upper_values[i] < upper_limit)
|
|
upper_limit = upper_values[i];
|
|
}
|
|
|
|
/* Build the probability density polynomial for the current
|
|
* subinterval.
|
|
*/
|
|
for (i = 0; i < num_moves; i++) {
|
|
if (i != k && upper_values[i] >= upper_limit) {
|
|
polynomial[++degree] = 0.0;
|
|
for (j = degree; j > 0; j--) {
|
|
polynomial[j] = (densities[i]
|
|
* (polynomial[j - 1]
|
|
+ ((lower_limit - lower_values[i])
|
|
* polynomial[j])));
|
|
}
|
|
|
|
polynomial[0] *= densities[i] * (lower_limit - lower_values[i]);
|
|
}
|
|
}
|
|
|
|
/* And compute the integral of the polynomial on the current
|
|
* subinterval.
|
|
*/
|
|
span_power = 1.0;
|
|
for (j = 0; j <= degree; j++) {
|
|
span_power *= upper_limit - lower_limit;
|
|
probabilities[moves[k]] += (polynomial[j] * span_power) / (j + 1);
|
|
}
|
|
|
|
/* Go on to the next subinterval. */
|
|
lower_limit = upper_limit;
|
|
}
|
|
|
|
probabilities[moves[k]] *= densities[k];
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Local Variables:
|
|
* tab-width: 8
|
|
* c-basic-offset: 2
|
|
* End:
|
|
*/
|