You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdegames/kreversi/Engine.cpp

788 lines
22 KiB

/* Yo Emacs, this -*- C++ -*-
*******************************************************************
*******************************************************************
*
*
* KREVERSI
*
*
*******************************************************************
*
* A Reversi (or sometimes called Othello) game
*
*******************************************************************
*
* Created 1997 by Mario Weilguni <mweilguni@sime.com>. This file
* is ported from Mats Luthman's <Mats.Luthman@sylog.se> JAVA applet.
* Many thanks to Mr. Luthman who has allowed me to put this port
* under the GNU GPL. Without his wonderful game engine kreversi
* would be just another of those Reversi programs a five year old
* child could beat easily. But with it it's a worthy opponent!
*
* If you are interested on the JAVA applet of Mr. Luthman take a
* look at http://www.sylog.se/~mats/
*
*******************************************************************
*
* This file is part of the KDE project "KREVERSI"
*
* KREVERSI 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; either version 2, or (at your option)
* any later version.
*
* KREVERSI 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 KREVERSI; see the file COPYING. If not, write to
* the Free Software Foundation, 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*******************************************************************
*/
// The class Engine produces moves from a Game object through calls to the
// function ComputeMove().
//
// First of all: this is meant to be a simple example of a game playing
// program. Not everything is done in the most clever way, particularly not
// the way the moves are searched, but it is hopefully made in a way that makes
// it easy to understand. The function ComputeMove2() that does all the work
// is actually not much more than a hundred lines. Much could be done to
// make the search faster though, I'm perfectly aware of that. Feel free
// to experiment.
//
// The method used to generate the moves is called minimax tree search with
// alpha-beta pruning to a fixed depth. In short this means that all possible
// moves a predefined number of moves ahead are either searched or refuted
// with a method called alpha-beta pruning. A more thorough explanation of
// this method could be found at the world wide web at http:
// //yoda.cis.temple.edu:8080/UGAIWWW/lectures96/search/minimax/alpha-beta.html
// at the time this was written. Searching for "minimax" would also point
// you to information on this subject. It is probably possible to understand
// this method by reading the source code though, it is not that complicated.
//
// At every leaf node at the search tree, the resulting position is evaluated.
// Two things are considered when evaluating a position: the number of pieces
// of each color and at which squares the pieces are located. Pieces at the
// corners are valuable and give a high value, and having pieces at squares
// next to a corner is not very good and they give a lower value. In the
// beginning of a game it is more important to have pieces on "good" squares,
// but towards the end the total number of pieces of each color is given a
// higher weight. Other things, like how many legal moves that can be made in a
// position, and the number of pieces that can never be turned would probably
// make the program stronger if they were considered in evaluating a position,
// but that would make things more complicated (this was meant to be very
// simple example) and would also slow down computation (considerably?).
//
// The member m_board[10][10]) holds the current position during the
// computation. It is initiated at the start of ComputeMove() and
// every move that is made during the search is made on this board. It should
// be noted that 1 to 8 is used for the actual board, but 0 and 9 can be
// used too (they are always empty). This is practical when turning pieces
// when moves are made on the board. Every piece that is put on the board
// or turned is saved in the stack m_squarestack (see class SquareStack) so
// every move can easily be reversed after the search in a node is completed.
//
// The member m_bc_board[][] holds board control values for each square
// and is initiated by a call to the function private void SetupBcBoard()
// from Engines constructor. It is used in evaluation of positions except
// when the game tree is searched all the way to the end of the game.
//
// The two members m_coord_bit[9][9] and m_neighbor_bits[9][9] are used to
// speed up the tree search. This goes against the principle of keeping things
// simple, but to understand the program you do not need to understand them
// at all. They are there to make it possible to throw away moves where
// the piece that is played is not adjacent to a piece of opposite color
// at an early stage (because they could never be legal). It should be
// pointed out that not all moves that pass this test are legal, there will
// just be fewer moves that have to be tested in a more time consuming way.
//
// There are also two other members that should be mentioned: Score m_score
// and Score m_bc_score. They hold the number of pieces of each color and
// the sum of the board control values for each color during the search
// (this is faster than counting at every leaf node).
//
// The classes SquareStackEntry and SquareStack implement a
// stack that is used by Engine to store pieces that are turned during
// searching (see ComputeMove()).
//
// The class MoveAndValue is used by Engine to store all possible moves
// at the first level and the values that were calculated for them.
// This makes it possible to select a random move among those with equal
// or nearly equal value after the search is completed.
#include <tqapplication.h>
#include "Engine.h"
// ================================================================
// Class ULONG64
#if !defined(__GNUC__)
ULONG64::ULONG64() : TQBitArray(64)
{
fill(0);
}
// Initialize an ULONG64 from a 32 bit value.
//
ULONG64::ULONG64( unsigned int value ) : TQBitArray(64)
{
fill(0);
for(int i = 0; i < 32; i++) {
setBit(i, (bool)(value & 1));
value >>= 1;
}
}
// Shift an ULONG64 left one bit.
//
void ULONG64::shl()
{
for(int i = 63; i > 0; i--)
setBit(i, testBit(i - 1));
setBit(0, 0);
}
#endif
// ================================================================
// Classes SquareStackEntry and SquareStack
// A SquareStack is used to store changes to the squares on the board
// during search.
inline void SquareStackEntry::setXY(int x, int y) {
m_x = x;
m_y = y;
}
SquareStackEntry::SquareStackEntry()
{
setXY(0,0);
}
// ----------------------------------------------------------------
SquareStack::SquareStack() {
init(0);
}
SquareStack::SquareStack(int size) {
init(size);
}
void SquareStack::resize(int size)
{
m_squarestack.resize(size);
}
// (Re)initialize the stack so that is empty, and at the same time
// resize it to 'size'.
//
void SquareStack::init(int size)
{
resize(size);
m_top = 0;
for (int i = 0; i < size; i++)
m_squarestack[i].setXY(0,0);
}
inline SquareStackEntry SquareStack::Pop()
{
return m_squarestack[--m_top];
}
inline void SquareStack::Push(int x, int y)
{
m_squarestack[m_top].m_x = x;
m_squarestack[m_top++].m_y = y;
}
// ================================================================
// Class MoveAndValue
// Class MoveAndValue aggregates a move with its value.
//
inline void MoveAndValue::setXYV(int x, int y, int value)
{
m_x = x;
m_y = y;
m_value = value;
}
MoveAndValue::MoveAndValue()
{
setXYV(0,0,0);
}
MoveAndValue::MoveAndValue(int x, int y, int value)
{
setXYV(x, y, value);
}
// ================================================================
// The Engine itself
// Some special values used in the search.
const int Engine::LARGEINT = 99999;
const int Engine::ILLEGAL_VALUE = 8888888;
const int Engine::BC_WEIGHT = 3;
Engine::Engine(int st, int sd) : SuperEngine(st, sd)
{
SetupBcBoard();
SetupBits();
}
Engine::Engine(int st) : SuperEngine(st)
{
SetupBcBoard();
SetupBits();
}
Engine::Engine() : SuperEngine(1)
{
SetupBcBoard();
SetupBits();
}
// keep GUI alive
void Engine::yield()
{
tqApp->processEvents();
}
// Calculate the best move from the current position, and return it.
Move Engine::computeMove(Game *game, bool competitive)
{
Color color;
// A competitive game is one where we try our damnedest to make the
// best move. The opposite is a casual game where the engine might
// make "a mistake". The idea behind this is not to scare away
// newbies. The member m_competitive is used during search for this
// very move.
m_competitive = competitive;
// Suppose that we should give a heuristic evaluation. If we are
// close to the end of the game we can make an exhaustive search,
// but that case is determined further down.
m_exhaustive = false;
// Get the color to calculate the move for.
color = game->toMove();
if (color == Nobody)
return Move(Nobody, -1, -1);
// Figure out the current score
m_score.set(White, game->score(White));
m_score.set(Black, game->score(Black));
// Treat the first move as a special case (we can basically just
// pick a move at random).
if (m_score.score(White) + m_score.score(Black) == 4)
return ComputeFirstMove(game);
// Let there be room for 3000 changes during the recursive search.
// This is more than will ever be needed.
m_squarestack.init(3000);
// Get the search depth. If we are close to the end of the game,
// the number of possible moves goes down, so we can search deeper
// without using more time.
m_depth = m_strength;
if (m_score.score(White) + m_score.score(Black) + m_depth + 3 >= 64)
m_depth = 64 - m_score.score(White) - m_score.score(Black);
else if (m_score.score(White) + m_score.score(Black) + m_depth + 4 >= 64)
m_depth += 2;
else if (m_score.score(White) + m_score.score(Black) + m_depth + 5 >= 64)
m_depth++;
// If we are very close to the end, we can even make the search
// exhaustive.
if (m_score.score(White) + m_score.score(Black) + m_depth >= 64)
m_exhaustive = true;
// The evaluation is a linear combination of the score (number of
// pieces) and the sum of the scores for the squares (given by
// m_bc_score). The earlier in the game, the more we use the square
// values and the later in the game the more we use the number of
// pieces.
m_coeff = 100 - (100*
(m_score.score(White) + m_score.score(Black)
+ m_depth - 4)) / 60;
// Initialize the board that we use for the search.
for (uint x = 0; x < 10; x++)
for (uint y = 0; y < 10; y++) {
if (1 <= x && x <= 8
&& 1 <= y && y <= 8)
m_board[x][y] = game->color(x, y);
else
m_board[x][y] = Nobody;
}
// Initialize a lot of stuff that we will use in the search.
// Initialize m_bc_score to the current bc score. This is kept
// up-to-date incrementally so that way we won't have to calculate
// it from scratch for each evaluation.
m_bc_score.set(White, CalcBcScore(White));
m_bc_score.set(Black, CalcBcScore(Black));
ULONG64 colorbits = ComputeOccupiedBits(color);
ULONG64 opponentbits = ComputeOccupiedBits(opponent(color));
int maxval = -LARGEINT;
int max_x = 0;
int max_y = 0;
MoveAndValue moves[60];
int number_of_moves = 0;
int number_of_maxval = 0;
setInterrupt(false);
ULONG64 null_bits;
null_bits = 0;
// The main search loop. Step through all possible moves and keep
// track of the most valuable one. This move is stored in
// (max_x, max_y) and the value is stored in maxval.
m_nodes_searched = 0;
for (int x = 1; x < 9; x++) {
for (int y = 1; y < 9; y++) {
// Don't bother with non-empty squares and squares that aren't
// neighbors to opponent pieces.
if (m_board[x][y] != Nobody
|| (m_neighbor_bits[x][y] & opponentbits) == null_bits)
continue;
int val = ComputeMove2(x, y, color, 1, maxval,
colorbits, opponentbits);
if (val != ILLEGAL_VALUE) {
moves[number_of_moves++].setXYV(x, y, val);
// If the move is better than all previous moves, then record
// this fact...
if (val > maxval) {
// ...except that we want to make the computer miss some
// good moves so that beginners can play against the program
// and not always lose. However, we only do this if the
// user wants a casual game, which is set in the settings
// dialog.
int randi = m_random.getLong(7);
if (maxval == -LARGEINT
|| m_competitive
|| randi < (int) m_strength) {
maxval = val;
max_x = x;
max_y = y;
number_of_maxval = 1;
}
}
else if (val == maxval)
number_of_maxval++;
}
// Jump out prematurely if interrupt is set.
if (interrupted())
break;
}
}
// long endtime = times(&tmsdummy);
// If there are more than one best move, the pick one randomly.
if (number_of_maxval > 1) {
int r = m_random.getLong(number_of_maxval) + 1;
int i;
for (i = 0; i < number_of_moves; i++) {
if (moves[i].m_value == maxval && --r <= 0)
break;
}
max_x = moves[i].m_x;
max_y = moves[i].m_y;
}
// Return a suitable move.
if (interrupted())
return Move(Nobody, -1, -1);
else if (maxval != -LARGEINT)
return Move(color, max_x, max_y);
else
return Move(Nobody, -1, -1);
}
// Get the first move. We can pick any move at random.
//
Move Engine::ComputeFirstMove(Game *game)
{
int r;
Color color = game->toMove();
r = m_random.getLong(4) + 1;
if (color == White) {
if (r == 1) return Move(color, 3, 5);
else if (r == 2) return Move(color, 4, 6);
else if (r == 3) return Move(color, 5, 3);
else return Move(color, 6, 4);
}
else {
if (r == 1) return Move(color, 3, 4);
else if (r == 2) return Move(color, 5, 6);
else if (r == 3) return Move(color, 4, 3);
else return Move(color, 6, 5);
}
}
// Play a move at (xplay, yplay) and generate a value for it. If we
// are at the maximum search depth, we get the value by calling
// EvaluatePosition(), otherwise we get it by performing an alphabeta
// search.
//
int Engine::ComputeMove2(int xplay, int yplay, Color color, int level,
int cutoffval, ULONG64 colorbits,
ULONG64 opponentbits)
{
int number_of_turned = 0;
SquareStackEntry mse;
Color opponent = ::opponent(color);
m_nodes_searched++;
// Put the piece on the board and incrementally update scores and bitmaps.
m_board[xplay][yplay] = color;
colorbits |= m_coord_bit[xplay][yplay];
m_score.inc(color);
m_bc_score.add(color, m_bc_board[xplay][yplay]);
// Loop through all 8 directions and turn the pieces that can be turned.
for (int xinc = -1; xinc <= 1; xinc++)
for (int yinc = -1; yinc <= 1; yinc++) {
if (xinc == 0 && yinc == 0)
continue;
int x, y;
for (x = xplay + xinc, y = yplay + yinc; m_board[x][y] == opponent;
x += xinc, y += yinc)
;
// If we found the end of a turnable row, then go back and turn
// all pieces on the way back. Also push the squares with
// turned pieces on the squarestack so that we can undo the move
// later.
if (m_board[x][y] == color)
for (x -= xinc, y -= yinc; x != xplay || y != yplay;
x -= xinc, y -= yinc) {
m_board[x][y] = color;
colorbits |= m_coord_bit[x][y];
opponentbits &= ~m_coord_bit[x][y];
m_squarestack.Push(x, y);
m_bc_score.add(color, m_bc_board[x][y]);
m_bc_score.sub(opponent, m_bc_board[x][y]);
number_of_turned++;
}
}
int retval = -LARGEINT;
// If we managed to turn at least one piece, then (xplay, yplay) was
// a legal move. Now find out the value of the move.
if (number_of_turned > 0) {
// First adjust the number of pieces for each side.
m_score.add(color, number_of_turned);
m_score.sub(opponent, number_of_turned);
// If we are at the bottom of the search, get the evaluation.
if (level >= m_depth)
retval = EvaluatePosition(color); // Terminal node
else {
int maxval = TryAllMoves(opponent, level, cutoffval, opponentbits,
colorbits);
if (maxval != -LARGEINT)
retval = -maxval;
else {
// No possible move for the opponent, it is colors turn again:
retval = TryAllMoves(color, level, -LARGEINT, colorbits, opponentbits);
if (retval == -LARGEINT) {
// No possible move for anybody => end of game:
int finalscore = m_score.score(color) - m_score.score(opponent);
if (m_exhaustive)
retval = finalscore;
else {
// Take a sure win and avoid a sure loss (may not be optimal):
if (finalscore > 0)
retval = LARGEINT - 65 + finalscore;
else if (finalscore < 0)
retval = -(LARGEINT - 65 + finalscore);
else
retval = 0;
}
}
}
}
m_score.add(opponent, number_of_turned);
m_score.sub(color, number_of_turned);
}
// Undo the move. Start by unturning the turned pieces.
for (int i = number_of_turned; i > 0; i--) {
mse = m_squarestack.Pop();
m_bc_score.add(opponent, m_bc_board[mse.m_x][mse.m_y]);
m_bc_score.sub(color, m_bc_board[mse.m_x][mse.m_y]);
m_board[mse.m_x][mse.m_y] = opponent;
}
// Now remove the new piece that we put down.
m_board[xplay][yplay] = Nobody;
m_score.sub(color, 1);
m_bc_score.sub(color, m_bc_board[xplay][yplay]);
// Return a suitable value.
if (number_of_turned < 1 || interrupted())
return ILLEGAL_VALUE;
else
return retval;
}
// Generate all legal moves from the current position, and do a search
// to see the value of them. This function returns the value of the
// most valuable move, but not the move itself.
//
int Engine::TryAllMoves(Color opponent, int level, int cutoffval,
ULONG64 opponentbits, ULONG64 colorbits)
{
int maxval = -LARGEINT;
// Keep GUI alive by calling the event loop.
yield();
ULONG64 null_bits;
null_bits = 0;
for (int x = 1; x < 9; x++) {
for (int y = 1; y < 9; y++) {
if (m_board[x][y] == Nobody
&& (m_neighbor_bits[x][y] & colorbits) != null_bits) {
int val = ComputeMove2(x, y, opponent, level+1, maxval, opponentbits,
colorbits);
if (val != ILLEGAL_VALUE && val > maxval) {
maxval = val;
if (maxval > -cutoffval || interrupted())
break;
}
}
}
if (maxval > -cutoffval || interrupted())
break;
}
if (interrupted())
return -LARGEINT;
return maxval;
}
// Calculate a heuristic value for the current position. If we are at
// the end of the game, do this by counting the pieces. Otherwise do
// it by combining the score using the number of pieces, and the score
// using the board control values.
//
int Engine::EvaluatePosition(Color color)
{
int retval;
Color opponent = ::opponent(color);
int score_color = m_score.score(color);
int score_opponent = m_score.score(opponent);
if (m_exhaustive)
retval = score_color - score_opponent;
else {
retval = (100-m_coeff) *
(m_score.score(color) - m_score.score(opponent))
+ m_coeff * BC_WEIGHT * (m_bc_score.score(color)
- m_bc_score.score(opponent));
}
return retval;
}
// Calculate bitmaps for each square, and also for neighbors of each
// square.
//
void Engine::SetupBits()
{
//m_coord_bit = new long[9][9];
//m_neighbor_bits = new long[9][9];
ULONG64 bits = 1;
// Store a 64 bit unsigned it with the corresponding bit set for
// each square.
for (int i=1; i < 9; i++)
for (int j=1; j < 9; j++) {
m_coord_bit[i][j] = bits;
#if !defined(__GNUC__)
bits.shl();
#else
bits *= 2;
#endif
}
// Store a bitmap consisting of all neighbors for each square.
for (int i=1; i < 9; i++)
for (int j=1; j < 9; j++) {
m_neighbor_bits[i][j] = 0;
for (int xinc=-1; xinc<=1; xinc++)
for (int yinc=-1; yinc<=1; yinc++) {
if (xinc != 0 || yinc != 0)
if (i + xinc > 0 && i + xinc < 9 && j + yinc > 0 && j + yinc < 9)
m_neighbor_bits[i][j] |= m_coord_bit[i + xinc][j + yinc];
}
}
}
// Set up the board control values that will be used in evaluation of
// the position.
//
void Engine::SetupBcBoard()
{
// JAVA m_bc_board = new int[9][9];
for (int i=1; i < 9; i++)
for (int j=1; j < 9; j++) {
if (i == 2 || i == 7)
m_bc_board[i][j] = -1;
else
m_bc_board[i][j] = 0;
if (j == 2 || j == 7)
m_bc_board[i][j] -= 1;
}
m_bc_board[1][1] = 2;
m_bc_board[8][1] = 2;
m_bc_board[1][8] = 2;
m_bc_board[8][8] = 2;
m_bc_board[1][2] = -1;
m_bc_board[2][1] = -1;
m_bc_board[1][7] = -1;
m_bc_board[7][1] = -1;
m_bc_board[8][2] = -1;
m_bc_board[2][8] = -1;
m_bc_board[8][7] = -1;
m_bc_board[7][8] = -1;
}
// Calculate the board control score.
//
int Engine::CalcBcScore(Color color)
{
int sum = 0;
for (int i=1; i < 9; i++)
for (int j=1; j < 9; j++)
if (m_board[i][j] == color)
sum += m_bc_board[i][j];
return sum;
}
// Calculate a bitmap of the occupied squares for a certain color.
//
ULONG64 Engine::ComputeOccupiedBits(Color color)
{
ULONG64 retval = 0;
for (int i=1; i < 9; i++)
for (int j=1; j < 9; j++)
if (m_board[i][j] == color) retval |= m_coord_bit[i][j];
return retval;
}