/**
* @file
* Main functions for the Qube object, pretty much the center of the game.
*
* Last Modified: Tue Dec 11 16:05:17 PST 2018
* @author Kevin O'Gorman
* @author Santiago Canez
*/
/*
* Copyright 2002 Santiago Canez <scanez@math.berkeley.edu>
* Copyright 2012-2016,2018 Kevin O'Gorman <kogorman@gmail.com>.
* Distributed under the GNU General Public License.
*
* This file is part of libqubist, the library of functions for playing
* 4x4x4 tic-tac-toe, also known as Qubic, and by some other names
* outside the USA.
*
* Libqubist 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 2.
*
* Libqubist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* @file
* Main functions for the Qube object, pretty much the center of the game.
*
* Main functions for the Qube object, pretty much the center of the game.
* The data structure is called the Qube, to fit in with the overall naming scheme.
* It contains the status of each cell (here called a Square) and Row, as well
* as a record of the moves made, and the moves under consideration.
*/
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include "player.h"
#include "board.h"
#include "row.h"
#include "iso.h"
#include "moves.h"
#include "base64.h"
/* BEWARE: debugging slows things down by a factor of around 100! This
* is particularly noticeable during recursive descent, such as occurs in validation.
*/
#undef NDEBUG
#define NDEBUG
#include <assert.h>
/** The default buffer size. */
#define BUFL 129
/* Avoid exporting symbols when not debugging. */
#ifdef NDEBUG
#define STATIC static
/** Macro to show a position. */
#define SHOWPOS
#else
#define STATIC
/** Macro to show a position. */
#define SHOWPOS \
{\
char posn[65];\
qb_write_qube_through_iso(b, 0, posn);\
printf("Position at " __FILE__ ", line %d is %s\n", __LINE__, posn);\
}
#endif
/**
* Print a cache entry.
* @param vstream the filestream to receive the printout.
* @param memo the entry
* @param label what to call it.
*/
void
print_cache(FILE *vstream, struct memo *memo, char *label)
{
char position[65];
memcpy(position, memo->posn, 64);
position[64] = '\0';
fprintf(vstream, "%s: hits %lld, posns: %lld, final census: %d, value: %d, census: %d, reached?: %d. %s\n",
position, memo->hits, memo->positions, memo->final_census, memo->return_value,
memo->start_census, memo->limit_reached, label);
}
STATIC int qb_get_best_move(Qube *b, ScoreTable * scoring);
STATIC int qb_find_forced_win(Qube *b, Player *us, SearchSettings *s, int *reached, int verbose, FILE *vstream);
STATIC int qb_get_hint_sq( Qube *b);
STATIC int qb_get_move_advanced(Qube *b);
/** Player profile */
Player qb_human = {
FALSE,
PLAYER1, PLAYER2, PLAYER3, PLAYER4,
COMP1, COMP2, COMP3, COMP4,
PLAYER,
COMP,
"Human player"
};
/** Computer profile */
Player qb_computer = {
TRUE,
COMP1, COMP2, COMP3, COMP4,
PLAYER1, PLAYER2, PLAYER3, PLAYER4,
COMP,
PLAYER,
"Computer"
};
/** Settings for Novice difficulty */
SearchSettings qb_novice = {
TRUE, /* can_hint */
FALSE, /* can_follow_reforce */
DEFAULT_SEARCH_DEPTH,
0, /* normal play */
NULL /* not writing positions during play */
};
/** The intermediate difficulty doesn't use a search, so no settings are defined. */
/** Settings for Advanced difficulty */
SearchSettings qb_advanced = {
FALSE, /* can_hint */
FALSE, /* can_follow_reforce */
DEFAULT_SEARCH_DEPTH,
0, /* normal play */
NULL /* not writing positions during play */
};
/** Settings for Expert difficulty */
SearchSettings qb_expert = {
FALSE, TRUE, 25,
0, /* normal play */
NULL /* not writing positions during play */
};
/** "External" Settings used for external calls. These are all for use by development code,
* not game playing.
*/
SearchSettings qb_external = {
FALSE, TRUE,
0, /* search depth limit must be filled in by the call */
0, /* set to 1 to traverse all possible moves */
NULL /* set by the call */
};
/* Scoring arrays for various difficulty levels. */
/**
* Scoring for the novice difficulty.
* We like 3's, dual 2's, empty rows, 2's, and 1's in that order. Negative entries are to
* trigger assertions that they are not used.
*/
static ScoreTable novice_scores = {
{
{ 7, 3, 6, 30,-99,-99,-99, 4, 3, 40, 1, 1}, /* first */
{ 7, 3, 10, 30,-99,-99,-99, 4, 20, 40, 1, 1}, /* subsequent */
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* unused */
/*P0 P1 P2 P3 P4 xx C0 C1 C2 C3 C4 MX */
}
};
/**
* Scoring for the intermediate difficulty.
* We like 3's, dual 2's, empty rows, 2's, and 1's in that order. Negative entries are to
* trigger assertions that they are not used.
*/
static ScoreTable intermediate_scores = {
{
{ 10, 3, 30, 158,-99,-99,-99, 4, 1,192, 1, 1}, /* first */
{ 10, 3, 32, 158,-99,-99,-99, 4, 88,192, 1, 1}, /* subsequent */
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* unused */
/*P0 P1 P2 P3 P4 xx C0 C1 C2 C3 C4 MX */
}
};
/**
* Create a timestamp string (for the log)
* @return a pointer to a static buffer containing a current timestamp.
*/
char *
qb_timestamp(void) {
time_t t;
struct tm *tmp;
static char stamp[256];
t = time(NULL);
tmp = localtime(&t);
if (tmp == NULL) {
perror("localtime");
exit(EXIT_FAILURE);
}
if (strftime(stamp, sizeof(stamp), "%d%b%y %H:%M:%S ", tmp) == 0) {
fprintf(stderr, "strftime returned 0\n");
exit(EXIT_FAILURE);
}
return stamp;
}
/**
* Log a string. A timestamp and the string are put to the logging filestream.
* @param b a pointer to the Qube
* @param s the string to log.
*/
void
qb_log(Qube *b, const char *s)
{
if (b->logfile) {
fprintf(b->logfile, "%s %s\n", qb_timestamp(), s);
}
}
/**
* Log two strings. A timestamp and the two strings are put to the logging filestream.
* @param b a pointer to the Qube
* @param s1 the first string to log.
* @param s2 the second string to log.
*/
void
qb_log2(Qube *b, const char *s1, const char *s2)
{
if (b->logfile) {
fprintf(b->logfile, "%s %s %s\n", qb_timestamp(), s1, s2);
}
}
/**
* Log a string and an integer. A timestamp, the string and the integer are put to the logging filestream.
* @param b a pointer to the Qube
* @param s1 the string to log.
* @param n the integer to log.
*/
void
qb_log_sn(Qube *b, const char *s1, int n)
{
if (b->logfile) {
fprintf(b->logfile, "%s %s %d\n", qb_timestamp(), s1, n);
}
}
Cache_table *theCache = NULL; /**< the cache table in use. */
size_t cache_multiplier; /**< the multiplier in use */
size_t cache_size; /**< the size of the cache table */
/** Hash function suggested by the article referenced in GNU hash.c:
B. J. McKenzie, R. Harries & T. Bell, Selecting a hashing algorithm,
Software--practice & experience 20, 2 (Feb 1990), 209-224. In that
article, careful selection of 'mult' and 'n_buckets' is recommended,
along with the warning that good choices depend on the problem
domain. Regardless of domain, it's probably a bad idea for the
multiplier and chache-size (n_buckets) to have a common factor.
Easy optimizations exist if either of them is a power of 2.
@param memo the item to be hashed
@return the entry number for this cache.
*/
size_t
cache_hasher(const struct memo *memo)
{
size_t value = 0;
int i;
for (i = 0; i < 64; i++) {
value = (value * cache_multiplier + ((unsigned char)(memo->posn[i]))) % cache_size;
}
return value;
}
/**
* Comparator of two cache entries, returning a boolean.
* @param a one item.
* @param b the other item.
* @return TRUE on equal, otherwise FALSE.
*/
bool cache_comparator(const struct memo *a, const struct memo *b) {
return strncmp(a->posn, b->posn, 64) == 0;
}
/**
* Initialize the cache table.
* @param size the desired number of entries. Zero for no cache at all.
* @param multiplier the factor to be used in hashing.
*/
void
qb_create_cache(size_t size, size_t multiplier) {
cache_size = size;
cache_multiplier = multiplier;
if (cache_size) {
theCache = cache_initialize(cache_size,
(Cache_hasher)cache_hasher,
(Cache_comparator)cache_comparator,
(Cache_data_freer)free);
if (!theCache) {
fprintf(stderr, " *** Cannot initialize the cache.\n");
exit(1);
}
} else {
theCache = NULL;
}
}
/**
* Create a board. The current directory is searched for a file named "qubiclog", and if found, it
* becomes the logfile; otherwise there is no logfile.
* @return a Qube object with a FILE * indicating the logging filestram, or NULL.
*/
Qube *
qb_new_qube(void) {
Qube *result = (Qube *)malloc(sizeof(Qube));
int fd;
if (!result) {
perror("creating a Qube");
exit(EXIT_FAILURE);
}
qb_initialize_qube(result);
fd=open("qubiclog", O_WRONLY);
if (fd >= 0) {
result->logfile = fdopen(fd, "a");
if (!result->logfile) {
perror("fdopen");
exit(EXIT_FAILURE);
}
setvbuf(result->logfile, NULL, _IONBF, 0);
qb_log(result, "Qube created.");
} else {
result->logfile = NULL;
}
result->positions = 0;
return result;
}
/**
* Free a board, and its resources. Any logfile is closed, and the Qube is free'd.
*/
void
qb_free_qube(Qube *board)
{
if (theCache) {
cache_free(theCache);
theCache = NULL;
}
if (board->logfile) {
qb_log(board, "Qube closed.");
fclose(board->logfile);
}
free(board);
}
/**
* Return one of the moves that have been made, by move number.
* @param b a pointer to the Qube object.
* @param i the move number.
* @return the square number of the indicated move.
*/
int
qb_get_move(Qube *b, int i)
{
return b->moves_made[i];
}
/**
* Test if the human passed.
* @param b a pointer to the Qube object.
* @return TRUE if the human passed; FALSE if not.
*/
int
qb_human_passed(Qube *b)
{
return qb_move_count(b) > 0 && qb_get_move(b, 0) == -1;
}
/**
* Return a list of the moves made so far.
* @param b a pointer to the Qube object.
* @return a pointer to a static string containing the list.
*/
char *
qb_get_move_list(Qube *b)
{
static char result[5*64];
char one[5];
int num;
int i;
result[0] = '\0';
num = qb_move_count(b);
for (i = 0; i < num; i++) {
sprintf(one, "%d, ", qb_get_move(b, i));
strcat(result, one);
}
i = strlen(result);
result[i-2] = '\0';
return result;
}
/**
* Return the Player object for the player to move.
* @param b a pointer to the Qube object.
* @return the Player object.
*/
Player *
qb_current_player(Qube *b)
{
return b->current_player;
}
/**
* Return the Player not on turn.
* @param b a pointer to the Qube object.
* @return the Player not on turn.
*/
Player *
qb_other_player(Qube *b)
{
return OPPONENT(b->current_player);
}
/**
* Set the difficulty level to be played.
* @param b a pointer to the Qube object.
* @param level the difficulty level to be played.
*/
void
qb_set_difficulty(Qube *b, int level)
{
static SearchSettings *strats[] = {&qb_novice, 0, &qb_advanced, &qb_expert};
b->difficulty = level;
b->mySettings = strats[level];
}
/**
* Get the current difficulty level.
* @param b a pointer to the Qube object.
* @return the current difficulty level (int).
*/
int
qb_get_difficulty(Qube *b)
{
return b->difficulty;
}
/**
* (Re-)initialize a new board, create and initialize arrays and settings.
* @param b the Qube object.
*/
void qb_initialize_qube(Qube *b) {
qb_initialize_squares(b);
qb_initialize_rows(b);
b->current_player = &qb_human;
b->num_moves = b->num_tried = 0;
b->moves_for_win = 0;
b->positions = 0;
b->difficulty = INTERMEDIATE;
}
/**
* Return the length of the winning sequence, if any.
* @param b a pointer to the Qube object.
* @return the length of the winning sequence, or else -1.
*/
int
qb_win_length(Qube *b)
{
return b->moves_for_win;
}
/**
* Return the number of positions tried, including duplicates
* from transpositions. This is a running count, zeroed only when the Qube
* is first created.
* @param b a pointer to the Qube object.
* @return the number of positions tried, including duplicates
* from transpositions.
*/
unsigned long long int
qb_positions(Qube *b)
{
return b->positions;
}
/**
* Set a board position from a descriptive string.
* @param b the Qube object.
* @param marks a description using 64 marks chosen from {'-', 'o', 'x'}
*/
void
qb_internalize_qube(Qube *b, char *marks)
{
int i;
qb_initialize_qube(b);
for (i = 0; i < 64; i++) {
if (marks[i] == 'x') {b->squares[i].square_state = COMP; b->moves_made[b->num_moves++] = -1; }
else if (marks[i] == 'o') {b->squares[i].square_state = PLAYER; b->moves_made[b->num_moves++] = -1; }
else b->squares[i].square_state = EMPTY;
}
if (b->num_moves % 2) {
b->current_player = &qb_human;
} else {
b->current_player = &qb_computer;
}
b->num_tried = b->num_moves; /* satisfy precondition of qb_find_forced_win */
qb_evaluate_rows(b);
}
/**
* Return the number of moves that have actually been made.
* @param b a pointer to the Qube object.
* @return the number of moves that have been made.
*/
int
qb_move_count(Qube *b)
{
return b->num_moves;
}
/**
* Occupy a square on behalf of a given player. Update the row states of the rows where
* the square appears. Adjust b->num_tried, but not b->num_moves.
* @param b the Qube object.
* @param spot a pointer to the Square to occupy.
* @param who the identity of the player.
*/
STATIC void
qb_take_square_from_pointer(Qube *b, Square *spot, Player* who)
{
int rownum;
assert(qb_is_square_empty(spot));
assert(b->num_tried < 64);
spot->square_state = who->this_mark;
b->moves_made[b->num_tried++] = qb_get_square_num(spot);
for (rownum = 0; rownum < spot->num_rows; rownum++) {
b->rows[spot->rows_in[rownum]].row_state = qb_determine_row_state(b->rows + spot->rows_in[rownum]);
}
assert(qb_has_valid_rows(b));
}
/**
* Occupy a square on behalf of a given player. Update the row states of the rows where
* the square appears. Adjust b->num_tried, but not b->num_moves.
* @param b the Qube object.
* @param sq_num the index of the Square to occupy.
* @param who the identity of the player.
*/
void
qb_take_square(Qube *b, int sq_num, Player* who)
{
assert((unsigned)sq_num < 64);
qb_take_square_from_pointer(b, qb_get_square(b, sq_num), who);
}
/**
* Empty a square. Update the row states of the rows where the square appears.
* Adjust b->num_tried but not b->num_moves.
* @param b the Qube object.
* @param spot a pointer to the Square to clear.
*/
void
qb_untake_square_from_pointer(Qube *b, Square *spot)
{
int rownum;
assert(!qb_is_square_empty(spot));
assert(qb_get_square_num(spot) == b->moves_made[b->num_tried - 1]);
assert(b->num_tried > 0);
b->num_tried--;
spot->square_state = EMPTY;
for (rownum = 0; rownum < spot->num_rows; rownum++) {
b->rows[spot->rows_in[rownum]].row_state = qb_determine_row_state(b->rows + spot->rows_in[rownum]);
}
assert(qb_has_valid_rows(b));
}
/**
* Empty a square by number. Update the row states of the rows where the square appears.
* Adjust b->num_tried but not b->num_moves.
* @param b the Qube object.
* @param sq_num the index of the Square to clear.
*/
void
qb_untake_square(Qube *b, int sq_num)
{
assert((unsigned)sq_num < 64);
qb_untake_square_from_pointer(b, qb_get_square(b, sq_num));
}
/**
* Make a move for the current player. Adjust row-values as needed.
* Switch the current player. Note that the very first move may be a pass by the human player,
* allowing the computer to begin the game.
* @param b the Qubic board (Qube).
* @param sq_num the index of the square in the Qube.
* @return TRUE if this is possible, FALSE otherwise.
*/
int
qb_make_move(Qube *b, int sq_num)
{
int rownum;
Square* spot;
if (sq_num == -1 && b->num_moves == 0) {
/* A first move can be -1, indicating that the human passed, letting the computer start */
b->moves_made[b->num_moves++] = sq_num;
b->num_tried = b->num_moves;
b->current_player = OPPONENT(b->current_player);
return TRUE;
}
assert((unsigned)sq_num < 64);
spot = qb_get_square(b, sq_num);
if (spot->square_state == EMPTY) {
qb_take_square(b, sq_num, b->current_player);
b->num_moves = b->num_tried; /* real move */
b->current_player = OPPONENT(b->current_player);
for (rownum = 0; rownum < spot->num_rows; rownum++) {
b->rows[spot->rows_in[rownum]].row_state = qb_determine_row_state(b->rows + spot->rows_in[rownum]);
}
return TRUE;
}
return FALSE;
}
/**
* Reverse of qb_make_move: unmake the move, and adjust row-values as needed.
* Switch the current player.
* @param b the Qubic board (Qube).
*/
void
qb_unmake_move(Qube *b)
{
int sq_num;
assert(b->num_moves > 0);
sq_num = b->moves_made[b->num_tried - 1];
if (sq_num == -1) {
b->num_tried--; /* fake move */
} else {
qb_untake_square(b, sq_num); /* real move */
}
b->num_moves = b->num_tried;
b->current_player = OPPONENT(b->current_player);
}
/**
* Check if a pair of square numbers are forcing on behalf of the given player.
* @param b a pointer to the Qube object.
* @param sq1 one of the squares.
* @param sq2 the other square.
* @param who a pointer to the player.
* @return TRUE if the two moves are found to be forcing in some row, and FALSE otherwise.
*/
int
qb_is_forcing_pair(Qube *b, int sq1, int sq2, Player *who) {
Square *sp1;
Square *sp2;
int rowcount;
int rowi;
int rownum;
Row *rp;
int i;
sp1 = qb_get_square(b, sq1);
sp2 = qb_get_square(b, sq2);
rowcount = qb_get_row_count(sp1);
for (rowi = 0; rowi < rowcount; rowi++) {
rownum = qb_get_row_from_square(sp1, rowi);
rp = qb_get_row(b, rownum);
for (i = 0; i < 4; i++) {
if(qb_get_square_from_row(rp, i) == sp2) {
/* there will be only one row, so it must be one that forces */
return qb_get_row_state(rp) == who->this2;
}
}
}
return FALSE;
}
/**
* Check if a given square number is forcing on behalf of the given player.
* @param b a pointer to the Qube object.
* @param sq the number of a square.
* @param who a pointer to the player.
* @return TRUE if the player can force the opponent by taking the given square, and FALSE
* otherwise.
*/
int
qb_is_forcing_square(Qube *b, int sq, Player *who) {
Square *sp;
int rowcount;
int rowi;
int rownum;
Row *rp;
sp = qb_get_square(b, sq);
rowcount = qb_get_row_count(sp);
for (rowi = 0; rowi < rowcount; rowi++) {
rownum = qb_get_row_from_square(sp, rowi);
rp = qb_get_row(b, rownum);
if (qb_get_row_state(rp) == who->this2) {
return TRUE;
}
}
return FALSE;
}
/**
* Check if the board has been filled, and thus reached a draw.
* @param b a pointer to the Qube object.
* @return TRUE if a draw has been reached, and otherwise FALSE.
*/
int
qb_is_draw(Qube *b)
{
int sq;
for (sq = 0; sq < 64; sq++) {
if (b->squares[sq].square_state == EMPTY) {
return FALSE;
}
}
return TRUE;
}
/**
* Determine who won, if anyone.
* @param b the Qubic board (Qube).
* @return the Player object of the winner, if any. Otherwise return null.
*/
Player *
qb_game_over(Qube *b)
{
int i, state;
assert(qb_has_valid_rows(b));
for (i=0; i < 76; i++) { /* check each row for a win */
state = qb_get_row_state(b->rows + i);
if (state == COMP4) {
return &qb_computer;
} else if (state == PLAYER4) {
return &qb_human;
}
}
return 0;
}
/**
* Develop a string describing the winning move. Abort if there is none.
* @param b the Qubic board (Qube).
* @return a string describing the winning move.
*/
char*
qb_get_winning_row(Qube *b)
{
static char result[BUFL];
char *winner;
char *where;
int i, state;
assert(qb_has_valid_rows(b));
winner = b->current_player->name;
for (i=0; i < 76; i++) { /* find winning row */
state = qb_get_row_state(qb_get_row(b, i));
if (state == COMP4) {
winner = qb_computer.name;
} else if (state == PLAYER4) {
winner = qb_human.name;
} else {
continue;
}
where = qb_get_string_row(qb_get_row(b, i));
sprintf(result, "%s wins on row %s!\n", winner, where);
return result;
}
fputs(" *** Cannot get winning row from finished game.\n", stderr);
exit(EXIT_FAILURE);
}
/**
* Make a move as the computer. Use the current settings to decide what it should be.
* @param b the Qubic board (Qube).
* @return the index of the move.
*/
int
qb_make_comp_move(Qube *b)
{
int sq_num = -1;
int flag;
assert(qb_has_valid_rows(b));
assert(b->current_player == &qb_computer);
SHOWPOS
if (b->difficulty == NOVICE) {
sq_num = qb_get_best_move(b, &novice_scores);
} else if (b->difficulty == INTERMEDIATE) {
sq_num = qb_get_best_move(b, &intermediate_scores);
} else if (b->difficulty == ADVANCED || b->difficulty == EXPERT) {
sq_num = qb_get_move_advanced(b);
} else {
fprintf(stderr, "Unknown difficulty level.\n");
exit(EXIT_FAILURE);
}
SHOWPOS
/* make computer move.
* This assignment to 'flag' may throw a compiler warning when NDEBUG is defined, but the fixes make the
* code harder to read or introduce a Frankenbug. So I leave it alone, and tell the compiler to
* ignore such things.
*/
flag = qb_make_move(b, sq_num); /* warning suppressed */
assert(flag);
SHOWPOS
return sq_num;
}
/**
* Clear the board's squares completely, and reset row states.
* @param b the Qubic board (Qube).
*/
void
qb_clear_qube(Qube *b)
{
int i;
qb_clear_squares_array(b);
for (i=0; i < 64; i++) {
b->moves_made[i] = -1;
}
b->num_tried = b->num_moves = 0;
b->moves_for_win = -1;
b->current_player = &qb_human;
qb_evaluate_rows(b);
}
/**
* Undo moves to previous human move. This cancels any stored winning sequence.
* @param b a pointer to the Qube object.
*/
void
qb_undo_move(Qube *b)
{
assert(b->current_player == &qb_human);
if (b->num_tried > 0) {
if (qb_game_over(b) && (b->num_tried % 2)) {
/* Undoing first-player win, which is just a single move */
qb_untake_square(b, b->moves_made[b->num_tried - 1]);
b->current_player = OPPONENT(b->current_player);
} else {
qb_untake_square(b, b->moves_made[b->num_tried - 1]);
if (b->num_tried == 1 && b->moves_made[0] == -1) {
/* Undoing first-player pass. No actual move to undo, just the count. */
b->num_tried--;
} else {
/* Normal undo of two moves */
qb_untake_square(b, b->moves_made[b->num_tried - 1]);
}
}
}
b->num_moves = b->num_tried;
b->moves_for_win = -1;
}
/**
* Return the number of full rounds in the computed win sequence, if any.
* @param b a pointer to the Qube object.
* @return 0 if this is the beginner or intermediate difficulty, otherwise
* return the number of full rounds in the win sequence if there is one, otherwise
* return -1. The -1 is the marker left by initialization, not created here.
*/
int
qb_get_rounds_for_win(Qube *b)
{
if (b->difficulty >= ADVANCED && b->moves_for_win > 0) {
return (b->moves_for_win - b->num_moves) >> 1;
} else {
return 0;
}
}
/**
* Get the string describing a hint, or denying its availability, depending
* on the current difficulty level.
* @param b a pointer to the Qube object.
* @return a pointer to the printable string.
*/
char*
qb_get_hint(Qube *b)
{
static char hint[BUFL];
Square *sq;
if (b->difficulty == NOVICE) {
sq = qb_get_square(b, qb_get_hint_sq(b));
sprintf(hint, "Hint: %s\n", qb_get_string_square(sq));
return hint;
} else if (b->difficulty == INTERMEDIATE) {
return("What are you, a novice? No hint available in this difficulty!\n");
} else if (b->difficulty == ADVANCED) {
return("What!?! Against the advanced difficulty!? Be advanced! No hint for you...\n");
} else if (b->difficulty == EXPERT) {
return("What!?! Against the expert difficulty!? Be expert! No hint for you...\n");
} else {
fprintf(stderr, "Unknown difficulty level.\n");
exit(EXIT_FAILURE);
}
return hint;
}
/*************************************************************************************** Common search engine */
/**
* Recursive function to find a forced win by the player indicated by parameter 'us'.
* Returns only a truth-value, but any sequence found is remembered in the Qube object's moves,
* at least until the next time this function is called or a real move is made or un-made..
* Precondition: before the first call, num_tried must be set to num_moves, possibly by call qb_internalize_qube.
* @param b the Qube object.
* @param us a pointer to the Player object for the side to play.
* @param s a pointer to the SearchSettings (difficulty level) being played.
* @param reached pointer to a flag indicating if the limit was reached at least once. This is must
* be initialized to zero by the original caller for it to be meaningful.
* @param verbose the verbosity level. If verbose>1, detailed tracking is output on vstream.
* @param vstream the filestream to use for verbose output.
* @return TRUE if we found a win for the current player, FALSE otherwise.
*/
int
qb_find_forced_win(Qube *b, Player *us, SearchSettings *s, int *reached, int verbose, FILE *vstream)
{
int j, k;
Row *r;
Square *sq1;
Square *sq2 = (Square*)NULL;
char position[65];
int cell, row;
Player *them = OPPONENT(us);
int status;
int result;
unsigned long long int start_posns;
int limit = b->num_moves + 2*s->depth_limit;
int limit_reached;
const int indent = (b->num_tried - b->num_moves);
static char spaces[] = " "; /* 64 and a NUL */
const char *margin = spaces + 64 - indent;
#ifndef NDEBUG
int census_check = b->num_tried;
#endif
assert(qb_has_valid_rows(b));
assert (0 < b->num_moves);
assert (b->num_moves <= b->num_tried && b->num_tried < 64);
struct memo stacked_memo;
struct memo *found_memo, *replace_memo = NULL;
int memo_limit_reached = 0;
stacked_memo.hits = stacked_memo.positions = -1LL;
stacked_memo.final_census = stacked_memo.return_value = -1;
stacked_memo.start_census = b->num_tried;
stacked_memo.limit_reached = 0;
qb_write_qube_through_iso(b, 0, position);
memcpy(stacked_memo.posn, position, 64);
if (verbose > 1) {
fprintf(vstream, "\n%sSearching with census = %d\n", margin, b->num_tried);
qb_show_human(position, margin, vstream);
fputs(margin, vstream);
fflush(vstream);
}
start_posns = b->positions;
b->positions++;
/* Memoize from here.*/
if (theCache) {
replace_memo = cache_find_bucket(theCache, &stacked_memo);
if (replace_memo && cache_comparator(replace_memo, &stacked_memo)) {
found_memo = replace_memo;
} else {
found_memo = NULL;
}
if (verbose > 1) {
fprintf(vstream, "Lookup:\n");
fputs(margin, vstream);
print_cache(vstream, &stacked_memo, "Lookup key.");
fputs(margin, vstream);
if (found_memo) {
assert(found_memo->start_census == b->num_tried);
fprintf(vstream, "Position is in the cache.\n");
print_cache(vstream, found_memo, "Cached result found.");
} else {
fprintf(vstream, "Position is not cached.\n");
}
fputs(margin, vstream);
fflush(vstream);
}
/* Not all memoed results can be used.
* 1) if the found win was too deep to be used with the current level and SearchSettings,
* it must be ignored.
* 2) if the found loss was caused by reaching a depth limit, it can only be used if that
* limit is equal to or greater than the current limit. In that case, the current
* search would surely also fail.
* 3) If a found loss was caused by exhausting all possibilities without encountering the
* depth limit, it is always usable because no search can succeed.
*/
if (found_memo) {
if (found_memo->return_value) {
/* TODO: check this, since final_census is now one larger than it had been. */
/* Probably, just check by running the same taq twice, at its limit. */
if (found_memo->final_census >= limit ) {
found_memo = NULL; /* cannot use this */
if (verbose > 1) {
fprintf(vstream, "Discarding sequence that went deeper than the current limit.\n");
fputs(margin, vstream);
fflush(vstream);
}
}
} else {
if (found_memo->limit_reached) {
if (found_memo->limit_reached < limit) {
found_memo = NULL; /* cannot use this */
if (verbose > 1) {
fprintf(vstream, "Discarding depth-limited negative result from a shallower level.\n");
fputs(margin, vstream);
fflush(vstream);
}
}
}
}
}
} else {
/* no cache */
found_memo = NULL;
}
if (found_memo) {
/* use the memo */
b->positions += found_memo->positions;
assert(found_memo->start_census == b->num_tried);
found_memo->hits++;
if (reached && found_memo->limit_reached) {
*reached = found_memo->limit_reached;
}
result = found_memo->return_value;
if (result) {
b->moves_for_win = found_memo->final_census;
/* TODO: check that all the needed stuff is copied, and nothing more */
memcpy(&b->moves_made[b->num_tried], &found_memo->memo_moves[b->num_tried], sizeof(int) * (b->moves_for_win - b->num_tried));
}
} else {
/* no memo, or it cannot be used */
do {
/* Win detection. Note this can set moves_for_win at the depth limit, but no greater. */
if ((cell = qb_check_immediate_win(b, us)) != -1) {
b->moves_made[b->num_tried] = cell;
b->moves_for_win = b->num_tried + 1;
if (verbose > 1) {
fprintf(vstream, "%s wins at cell %d\n", us->name, cell);
fputs(margin, vstream);
fflush(vstream);
}
if (s->always_fail) {
result = FALSE;
} else {
result = TRUE;
}
break; /* Normally TRUE, but here may be forced FALSE to get a complete scan. */
}
/* Recursion stopper; will only look for a forced win up to depth_limit rounds deep */
if (b->num_tried >= limit) {
memo_limit_reached = 1;
if (reached) *reached = 1;
if (verbose > 1) {
fprintf(vstream, "Eeek! Recursion limit reached (%d >= %d).\n", b->num_tried, limit);
fputs(margin, vstream);
fflush(vstream);
}
continue; /* FALSE result */
}
/* Check if the opponent is forcing. If so, either call it a loss, or if the settings allow,
* check if the force is single and the reply is also forcing, continuing if so.
*/
if ((cell = qb_check_immediate_win(b, them)) != -1) {
if (!(s->can_follow_reforce)) {
if (verbose > 1) {
fprintf(vstream, "Eeek! %s is forced to %d, and at this difficulty level is giving up.\n", us->name, cell);
fputs(margin, vstream);
fflush(vstream);
}
continue; /* FALSE result */
}
/* If the opponent could win at 'cell', we are forced to go there to prevent it, and just
* hope it works.
*/
qb_take_square(b, cell, us);
/* The cell is now occupied. Check that there wasn't a second threat. */
if (qb_check_immediate_win(b, them) != -1) {
if (verbose > 1) {
fprintf(vstream, "Eeek! That was a double force. And we did it to ourself. Abort! Abort!\n");
fputs(margin, vstream);
}
qb_untake_square(b, cell);
continue; /* FALSE result */
}
/* It blocks, so it counts as trying. */
/* TODO: resolve with comment below about not trying */
b->positions++;
/* The cell is now occupied. Show posn if verbose, and apply sanity check if debugging */
if (verbose > 1) {
fprintf(vstream, "Eeek! %s is forced to %d; we'll take the cell and check that it is (re-)forcing.\n", us->name, cell);
qb_write_qube_through_iso(b, 0, position);
putc('\n', vstream);
qb_show_human(position, margin, vstream);
fputs(margin, vstream);
fflush(vstream);
}
assert(qb_has_valid_rows(b));
/* We need this move to be forcing, so we try to find a row that makes it true. */
for (row = 0; row < b->squares[cell].num_rows; row++) {
int forced_cell;
Row *rowp = &(b->rows[b->squares[cell].rows_in[row]]);
if (rowp->row_state == us->this3) {
/* the move to 'cell' is forcing. Hallelujia! Now find out which cell it forces to. */
forced_cell = qb_get_empty_from_row(b, rowp);
assert(forced_cell >= 0);
if (verbose > 1) {
putc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "Whew! It's okay, because it forces to %d\n", forced_cell);
fputs(margin, vstream);
/* being forced, it is not memoed and does not count as a move tried. */
fflush(vstream);
}
/* Occupy the cell */
qb_take_square(b, forced_cell, them);
/* Now do recursive call with this setup */
limit_reached = 0;
status = qb_find_forced_win(b, us, s, &limit_reached, verbose, vstream);
if (limit_reached) {
if (reached) {
*reached = limit_reached;
}
}
/* When done, untake the moves */
if (verbose > 1) {
fputc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "Untaking %d then %d",
b->moves_made[b->num_tried - 1],
b->moves_made[b->num_tried - 2]);
}
qb_untake_square(b, forced_cell);
qb_untake_square(b, cell);
assert(qb_has_valid_rows(b));
if (status) {
/* it won! */
if (verbose > 1) {
fputc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "Winning from force sequel.\n");
fputs(margin, vstream);
fflush(vstream);
}
} else {
/* It did not win. Report and give up because there was only one shot at this. */
if (verbose > 1) {
fputc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "Not winning in force sequel.\n");
fputs(margin, vstream);
fflush(vstream);
}
}
result = status;
goto all_break;
} else {
/* This row not forcing. If verbose, say so. Otherwise silently keep looking. */
if (verbose > 1) {
fprintf(vstream, "Row (%s) was not forcing for computer.", qb_get_string_row_linear(rowp));
fputc('\n', vstream);
fputs(margin, vstream);
fflush(vstream);
}
} /* This row was not forcing. */
} /* End of row search. */
/* Here, no row was forcing, and our blocking move is not tenable in a forcing sequence. */
if (verbose > 1) {
fputc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "This gives the %s an unforced move, so abandoning. ", them->name);
fflush(vstream);
}
qb_untake_square(b, cell);
assert(qb_has_valid_rows(b));
continue; /* FALSE result */
} /* End the case of a forcing move by opponent. */
/* opponent has no threat, so try all possible forcing moves */
for (row = 0; row < 76; row++) {
assert(qb_has_valid_rows(b));
r = qb_get_row(b, row);
/* handle only rows with 2 of our pieces */
if (r->row_state == us->this2) {
for (j=0; j < 4; j++) {
sq1 = qb_get_square_from_row(r, j);
/* handle both empty squares in this row */
if (qb_is_square_empty(sq1)) {
qb_take_square_from_pointer(b, sq1, us);
for (sq2=NULL, k=0; k < 4; k++) {
if (qb_is_square_empty(qb_get_square_from_row(r, k))) {
sq2 = qb_get_square_from_row(r, k);
break;
}
}
assert(sq2 != NULL);
qb_take_square_from_pointer(b, sq2, them);
if (s->freepos_stream) {
qb_write_qube_through_iso(b, 0, position);
fprintf(s->freepos_stream, "%s\n", position);
}
if (verbose > 1) {
fputc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "Row %d (%s), posn #%lld at %d(%c), depth %d, forcing %d(%c)",
row, qb_get_string_row_linear(r), b->positions,
qb_get_square_num(sq1), base64_encode(qb_get_square_num(sq1)),
b->num_tried - b->num_moves,
qb_get_square_num(sq2), base64_encode(qb_get_square_num(sq2)));
fflush(vstream);
}
/* recursive call */
limit_reached = 0;
status = qb_find_forced_win(b, us, s, &limit_reached, verbose, vstream);
if (limit_reached) {
memo_limit_reached = limit_reached;
if (reached) {
*reached = limit_reached;
}
}
if (verbose > 1) {
fputc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "Untaking %d then %d",
qb_get_square_num(sq2),
qb_get_square_num(sq1));
fflush(vstream);
}
qb_untake_square_from_pointer(b, sq2);
qb_untake_square_from_pointer(b, sq1);
if (status) {
/* found. */
if (verbose > 1) {
fputc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "That worked.\n");
fflush(vstream);
}
result = TRUE;
goto all_break;
} else {
/* not found. */
if (verbose > 1) {
fputc('\n', vstream);
fputs(margin, vstream);
fprintf(vstream, "That did not go well.\n");
fputc('\n', vstream);
fputs(margin, vstream);
fflush(vstream);
}
}
}
}
}
}
continue; /* FALSE result */
} while ((result = FALSE)); /* pretty sneaky, but see the following comment for worse. */
/* I hate doing this. This is the first goto target I've put in C code in several decades,
* but I don't see an easy way to avoid it, and right now I need easy. All this is doing, though,
* is providing a multi-level break from loops, so that all paths lead to creating the final memo.
* I think it's not as unstructured as some gotos.
* Slim comfort. Sigh.
* -- Kevin O'Gorman
*
* Further: the gotos could be eliminated by wrapping all those loops in another level of function
* call and having internal "return" statements instead of gotos. The gotos would be gone, but
* the result is arguably no better structured than this is.
*/
all_break:
assert(qb_has_valid_rows(b));
if (memo_limit_reached && reached) {
*reached = memo_limit_reached;
}
if (verbose > 1) {
if (result) {
fprintf(vstream, "%s winning", us->name);
} else {
fprintf(vstream, "%s not winning -- search continues", us->name);
}
fputc('\n', vstream);
fputs(margin, vstream);
fflush(vstream);
}
if (theCache) {
#ifndef NDEBUG
{
int i;
int dcnt = 0;
for (i = 0; i < 64; i++) {
if (stacked_memo.posn[i] != '-') dcnt++;
}
assert(dcnt == b->num_tried);
}
#endif
if (replace_memo) {
memcpy(replace_memo->posn, stacked_memo.posn, sizeof(stacked_memo.posn));
} else {
replace_memo = (struct memo*) malloc(sizeof(struct memo));
memcpy(replace_memo->posn, stacked_memo.posn, sizeof(stacked_memo.posn));
cache_miss(theCache, replace_memo);
}
replace_memo->hits = 1;
replace_memo->positions = b->positions - start_posns;
replace_memo->start_census = b->num_tried;
replace_memo->return_value = result;
memset(replace_memo->memo_moves, 0xff, sizeof(replace_memo->memo_moves));
if (result) {
replace_memo->final_census = b->moves_for_win;
memcpy(&replace_memo->memo_moves[b->num_moves], &b->moves_made[b->num_moves], sizeof(int) * (b->moves_for_win - b->num_moves + 1));
replace_memo->limit_reached = 0;
} else {
replace_memo->final_census = -1;
if (memo_limit_reached) {
replace_memo->limit_reached = limit;
} else {
replace_memo->limit_reached = 0;
}
}
if (verbose > 1) {
print_cache(vstream, replace_memo, "Cached result for this position.");
fputs(margin, vstream);
fflush(vstream);
}
}
}
if (verbose > 1) {
fprintf(vstream, "Returning %d.\n", result);
/* Do not indent, because level is about to change. */
}
#ifndef NDEBUG
assert(census_check == b->num_tried);
#endif
return result;
} /* end of qb_find_forced_win() */
/**
* Get a move for a forced win, or else from the advanced difficulty. This is for the opponent.
*/
int
qb_get_hint_sq(Qube *b)
{
int i;
assert(qb_has_valid_rows(b));
i = qb_check_immediate_win(b, &qb_human);
if (i != -1) return i;
i = qb_check_immediate_win(b, &qb_computer);
if (i != -1) return i;
b->num_tried = b->num_moves;
i = qb_find_forced_win(b, &qb_human, &qb_advanced, 0, 0, 0);
assert(b->num_tried == b->num_moves);
if (i) {
return b->moves_made[b->num_moves];
} else {
return qb_get_move_advanced(b);
}
}
/**
* Score a square according to a given set of weights.
* @param b the Qube object.
* @param s the Square to score.
* @param weights The weights for particular types of row.
* @return the score for that square.
*/
int
qb_score_square(Qube *b, Square *s, ScoreTable *weights)
{
int score = 0, i;
int types[MIXED + 1] = {0};
int size = s->num_rows;
Row *r;
assert(qb_has_valid_rows(b));
for (i=0; i < size; i++) {
r = qb_get_row(b, s->rows_in[i]);
types[qb_get_row_state(r)]++;
}
for (i=0; i <= MIXED; i++) {
if (types[i] > 1) {
assert(weights->weights[0][i] >=0);
score += weights->weights[0][i] + (types[i] - 1) * weights->weights[1][i];
} else if (types[i]) {
assert(weights->weights[0][i] >=0);
score += weights->weights[0][i];
}
}
return score;
}
/**
* Count the playable rows passing through a square. It's playable if it's
* profitable for the indicated player.
* @param b the Qube object.
* @param s the Square to score.
* @param weights The weights for particular types of row.
* @return the number of playable rows.
*/
int
qb_mycount_square(Qube *b, Square *s, ScoreTable *weights) {
int count = 0, i;
int size = s->num_rows;
Row *r;
for (i=0; i < size; i++) {
r = qb_get_row(b, s->rows_in[i]);
count += weights->weights[2][qb_get_row_state(r)];
}
return count;
}
/**
* Pick a square. Note that the scoring must make sure the
* right thing happens with immediate wins and opponent forces.
* @param b the Qube object.
* @param scoring a 2-dimensional array, with 1 row for the first time,
* and 1 for subsequent times of each square status.
* @return the square number.
*/
int
qb_get_best_move(Qube *b, ScoreTable *scoring)
{
int max = -1;
int result = -1;
int i;
int score;
assert(qb_has_valid_rows(b));
for (i=0; i < 64; i++) {
if (qb_is_square_empty(qb_get_square(b, i))) {
score = qb_score_square(b, qb_get_square(b, i), scoring);
if (score > max) {
max = score;
result = i;
}
}
}
return result;
}
char *qb_human_header_2 = "00-15 16-31 32-47 48-63";
char *qb_human_header_3 = " 0-F G-V W-l m-~";
/**
* Produce inidividual line of human-readable display of a position. There are 4 possible lines
* in this format, which is the one used in Oren's file of strategic moves.
* @param position the position as a string of 64 characters.
* @param skip the number of spaces between the planes.
* @param result a pointer to where the result should be built; must be at least 5 + 3*(4 + skip) bytes.
* @param line the line to produce (must be in the range 1-4 or it will regurn an empty string.)
*/
void
qb_show_human_line(char *position, int skip, char *result, int line)
{
int row, plane;
char *outptr;
memset(result, ' ', 4 + 3 * (4 + skip));
result[4 + 3 * (4 + skip)] = '\0';
switch (line) {
case 1:
case 2:
case 3:
case 4:
row = (line - 1) * 4;
outptr = result;
for (plane = 0; plane < 64; plane += 16) {
memcpy(outptr, &position[plane + row], 4);
outptr += 4 + skip;
}
break;
default:
result[0] = '\0';
break;
}
}
/**
* Show a position in a somewhat human-readable form, as four 4x4 squares of cells. The format
* is as used in Oren's file of strategic moves.
* @param position the position as a string of 64 characters.
* @param margin a string to use as a left margin (usually causes indentation).
* @param stream the filestream to send it to
*/
void
qb_show_human(char *position, const char *margin, FILE *stream)
{
int i, marks;
int row;
char save;
char scratch[50];
marks = 0;
for (i = 0; i < 64; i++) {
if (position[i] != '-') marks++;
}
save = position[64]; position[64] = '\0';
fprintf(stream, "%s%s census=%2.2d, cells 00-63\n", margin, position, marks);
position[64] = save;
fprintf(stream, "%s%s\n", margin, qb_human_header_2);
fprintf(stream, "%s%s\n", margin, qb_human_header_3);
for (row = 1; row <=4; row++) {
qb_show_human_line(position, 2, scratch, row);
fprintf(stream, "%s%s\n", margin, scratch);
}
}
/**
* Find a forced win. Intended as an entry-point for validation code only.
* Returns only a truth-value, but has informative side-effects.
* @param b the Qube object.
* @param limit a limit on recursion depth (in rounds).
* @param failflag TRUE to force failure; used to force visiting all possible positions. Not for use
* during play.
* @param reached pointer to a flag indicating if the limit was reached at least once. This is must
* be initialized to zero by the original caller for it to be meaningful.
* @param verbose the verbosity level. If verbose>1, detailed tracking is output on vstream.
* @param vstream the filestream to use for verbose output.
* @param freepos_stream if not NULL, the filestream to write all the examined free positions.
* @return TRUE if we found one, FALSE otherwise.
*/
int
qb_forced_win_limited(Qube *b, int limit, int failflag, int *reached, int verbose, FILE *vstream, FILE *freepos_stream)
{
int result;
qb_external.depth_limit = limit;
qb_external.freepos_stream = freepos_stream;
qb_external.always_fail = failflag;
result = qb_find_forced_win(b, &qb_computer, &qb_external, reached, verbose, vstream);
return result;
}
/**
* Find a move for the advanced difficulty. This always starts from scratch, looking
* for immediate urgent moves before looking (from scratch) for a forcing sequence.
* Note that this doubles as the EXPERT move finder, due to conditionals both here and
* in the forced-win function, and also as the NOVICE hint finder (where it finds what
* the computer would do and suggests that the human take it first.)
* @param b a Qubic board
* @return a move to take
*/
int
qb_get_move_advanced(Qube *b)
{
int i;
assert(qb_has_valid_rows(b));
assert(b->num_moves == b->num_tried);
#ifndef NDEBUG
printf("\nqb_get_move_advanced\n");
#endif
b->num_tried = b->num_moves;
b->moves_for_win = 0;
/* take a win if there is one */
i = qb_check_immediate_win(b, &qb_computer);
if (i != -1) {
return i;
}
/* block a win if necessary */
i = qb_check_immediate_win(b, &qb_human);
if (i != -1) {
return i;
}
SHOWPOS
/* if EXPERT, look for a strategic move */
if (b->difficulty == EXPERT) {
/* if computer had the first move and game is short enough. */
if (qb_get_move(b, 0) == -1 && qb_move_count(b) - 1 <= qb_get_max_census()) {
int iso;
char canonic[65];
int max;
int smove, realmove;
move_t *strat_array;
#ifndef NDEBUG
printf(" Looking for strategic move\n");
#endif
iso = qb_find_canonical_iso(b);
#ifndef NDEBUG
qb_write_qube_through_iso(b, 0, canonic);
printf(" Position is %s\n", canonic);
printf(" Best iso is %d\n", iso);
#endif
qb_write_qube_through_iso(b, iso, canonic);
#ifndef NDEBUG
printf(" Canonic position is %s\n", canonic);
#endif
max = qb_get_strat_count(qb_move_count(b) - 1);
#ifndef NDEBUG
printf(" About to search %d strategic positions.\n", max);
printf(" With weight %d.\n", qb_move_count(b) - 1);
printf("\n Trying ");
#endif
strat_array = qb_get_stratpos(qb_move_count(b) - 1);
while(max) {
#ifndef NDEBUG
printf(".");
#endif
if (strncmp(canonic, strat_array->posn, 64) == 0) {
/* ahah! we have a move */
smove = strat_array->move;
realmove = qb_move_from_iso(iso, smove);
#ifndef NDEBUG
printf("\n");
printf("Found strategic position %4d %s, %d\n", strat_array->id, strat_array->posn, strat_array->move);
printf("Found strategic move is really %d (%s)\n",
realmove,
qb_get_string_square(qb_get_square(b, realmove)));
#endif
return realmove;
}
max--;
strat_array++;
}
#ifndef NDEBUG
printf("\n");
printf("No strategic move found.\n");
#endif
}
#ifndef NDEBUG
else printf("Does not qualify for a strategic move.\n");
#endif
}
SHOWPOS
/*
* Try for a forced sequence using the current difficulty, not verbose, not reporting counts.
* or failing that, use the intermediate move routine (which has an expert mode)
*/
i = qb_find_forced_win(b, &qb_computer, b->mySettings, 0, 0, 0);
assert(b->num_moves == b->num_tried);
if (i) {
SHOWPOS
return b->moves_made[b->num_moves];
} else {
SHOWPOS
/* This might be a place for a warning that the strategic moves failed to find the expected forced win. */
return qb_get_best_move(b, &intermediate_scores);
}
}
/* vim: set expandtab autoindent shiftround nosmartindent tabstop=8 softtabstop=2 shiftwidth=2: */